├── test └── integration │ ├── tmp │ └── .gitignore │ ├── test_utils │ └── test-ipam-cni │ ├── README.md │ ├── sriov_mocked.go │ ├── libtest.sh │ ├── test_sriov_cni.sh │ └── test_concurrent.sh ├── OWNERS ├── .github ├── workflows │ ├── licensecheck.yml │ ├── fork-sync.yml │ ├── fork-ci.yml │ ├── static-scan.yml │ ├── codeql.yml │ ├── image-push-master.yml │ ├── image-push-release.yml │ └── buildtest.yml ├── ISSUE_TEMPLATE │ ├── enhancement.md │ ├── other.md │ └── bug-report.md └── dependabot.yml ├── images ├── build_docker.sh ├── image_test.sh ├── README.md ├── sriov-cni-daemonset.yaml └── entrypoint.sh ├── Dockerfile.nvidia ├── pkg ├── logging │ ├── logging_suite_test.go │ ├── logging.go │ └── logging_test.go ├── utils │ ├── utils_suite_test.go │ ├── mocks │ │ ├── pci_allocator_mock.go │ │ └── netlink_manager_mock.go │ ├── packet_test.go │ ├── pci_allocator_test.go │ ├── netlink_manager.go │ ├── pci_allocator.go │ ├── utils_test.go │ ├── testing.go │ ├── packet.go │ └── utils.go ├── config │ ├── config_suite_test.go │ ├── config.go │ └── config_test.go ├── sriov │ ├── sriov_suite_test.go │ └── mocks │ │ └── pci_utils_mock.go ├── types │ └── types.go └── cnicommands │ └── cni.go ├── Dockerfile ├── Developer.md ├── cmd └── sriov │ └── main.go ├── go.mod ├── .gitignore ├── .golangci.yml ├── make └── license.mk ├── docs └── configuration-reference.md ├── CONTRIBUTING.md ├── Makefile ├── go.sum ├── README.md └── LICENSE /test/integration/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | ## ADMINS: People who control settings for the repo. 2 | adrianchiris 3 | dougbtv 4 | 5 | ## Maintainers: People who can merge code in this repo. 6 | zeeke 7 | SchSeba 8 | Eoghan1232 9 | -------------------------------------------------------------------------------- /.github/workflows/licensecheck.yml: -------------------------------------------------------------------------------- 1 | name: License Check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main, master] 7 | 8 | jobs: 9 | call-license-check: 10 | uses: Mellanox/cloud-orchestration-reusable-workflows/.github/workflows/license-check-reusable.yml@main 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement / Feature Request 3 | about: Suggest an enhancement or new feature for SR-IOV CNI 4 | 5 | --- 6 | 7 | 8 | 9 | ## What would you like to be added? 10 | 11 | ## What is the use case for this feature / enhancement? 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other Issues 3 | about: For everything that isn't a bug report or a feature request 4 | 5 | --- 6 | 7 | 8 | ## What issue would you like to bring attention to? 9 | 10 | ## What is the impact of this issue? 11 | 12 | ## Do you have a proposed response or remediation for the issue? -------------------------------------------------------------------------------- /.github/workflows/fork-sync.yml: -------------------------------------------------------------------------------- 1 | name: Fork Sync 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # nightly 6 | workflow_dispatch: # enable manual trigger 7 | 8 | jobs: 9 | call-reusable-sync-fork-workflow: 10 | uses: Mellanox/cloud-orchestration-reusable-workflows/.github/workflows/fork-sync-reusable.yml@main 11 | with: 12 | upstream-owner: k8snetworkplumbingwg 13 | default-branch: master 14 | secrets: 15 | gh_token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /test/integration/test_utils/test-ipam-cni: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a fake implementation of an IP Address Management CNI plugin. This is used in the integration test suite 4 | # to simulate slow IPAM operations. 5 | 6 | if [[ -n "${IPAM_MOCK_SLEEP}" ]]; then 7 | sleep "${IPAM_MOCK_SLEEP}" 8 | fi 9 | 10 | if [[ "$CNI_COMMAND" == "DEL" ]]; then 11 | # CNI_DEL does not print any output on success 12 | exit 0 13 | fi 14 | 15 | cat << EOF 16 | { 17 | "cniVersion": "0.3.1", 18 | "interfaces": [{ 19 | "name": "${CNI_IFNAME}" 20 | }], 21 | "ips": [{ 22 | "name": "${CNI_IFNAME}", 23 | "address": "192.0.2.1/24" 24 | }] 25 | } 26 | EOF 27 | -------------------------------------------------------------------------------- /images/build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2025 sriov-cni authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | set -e 19 | 20 | ## Build docker image 21 | docker build -t sriov-cni -f ../Dockerfile ../ 22 | -------------------------------------------------------------------------------- /.github/workflows/fork-ci.yml: -------------------------------------------------------------------------------- 1 | name: Fork CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - network-operator-* 7 | tags: 8 | - network-operator-* 9 | 10 | jobs: 11 | call-reusable-ci-fork-workflow: 12 | uses: Mellanox/cloud-orchestration-reusable-workflows/.github/workflows/fork-ci-reusable.yml@main 13 | with: 14 | registry-internal: nvcr.io/nvstaging/mellanox 15 | service-account-username: nvidia-ci-cd 16 | service-account-email: svc-cloud-orch-gh@nvidia.com 17 | components: '[{"name": "SriovCni", "imageName": "sriov-cni", "Dockerfile": "Dockerfile.nvidia"}]' 18 | secrets: 19 | registry-username: ${{ secrets.NVCR_USERNAME }} 20 | registry-token: ${{ secrets.NVCR_TOKEN }} 21 | cicd-gh-token: ${{ secrets.GH_TOKEN_NVIDIA_CI_CD }} 22 | goproxy: ${{ secrets.GO_PROXY_URL }} 23 | -------------------------------------------------------------------------------- /Dockerfile.nvidia: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE_GO_DISTROLESS_DEV 2 | 3 | FROM golang:1.24-alpine as builder 4 | 5 | ARG GOPROXY 6 | ENV GOPROXY=$GOPROXY 7 | 8 | COPY . /usr/src/sriov-cni 9 | 10 | ENV HTTP_PROXY $http_proxy 11 | ENV HTTPS_PROXY $https_proxy 12 | 13 | WORKDIR /usr/src/sriov-cni 14 | RUN apk add --no-cache --virtual build-dependencies build-base=~0.5 && \ 15 | make clean && \ 16 | make build 17 | 18 | FROM ${BASE_IMAGE_GO_DISTROLESS_DEV:-nvcr.io/nvidia/distroless/go:v3.2.1-dev} 19 | 20 | USER 0:0 21 | SHELL ["/busybox/sh", "-c"] 22 | RUN ln -s /busybox/sh /bin/sh 23 | 24 | COPY --from=builder /usr/src/sriov-cni/build/sriov /usr/bin/ 25 | WORKDIR / 26 | 27 | LABEL io.k8s.display-name="SR-IOV CNI" 28 | 29 | COPY ./images/entrypoint.sh / 30 | # Copy the source code to the image 31 | COPY . /src 32 | 33 | ENTRYPOINT ["/entrypoint.sh"] 34 | -------------------------------------------------------------------------------- /.github/workflows/static-scan.yml: -------------------------------------------------------------------------------- 1 | name: Go-static-analysis 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: Lint 6 | runs-on: ubuntu-24.04 7 | steps: 8 | - name: Set up Go 9 | uses: actions/setup-go@v5 10 | with: 11 | go-version: "1.24" 12 | - uses: actions/checkout@v4 13 | - name: run make lint 14 | run: make lint 15 | shellcheck: 16 | name: Shellcheck 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Run ShellCheck 21 | uses: ludeeus/action-shellcheck@master 22 | with: 23 | check_together: yes 24 | hadolint: 25 | runs-on: ubuntu-24.04 26 | name: Hadolint 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: hadolint/hadolint-action@v3.1.0 30 | name: Run Hadolint 31 | with: 32 | dockerfile: Dockerfile 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Docker images 4 | - package-ecosystem: "docker" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | commit-message: 9 | prefix: "chore:" 10 | 11 | # GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | day: "monday" 17 | 18 | - package-ecosystem: "gomod" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | day: "tuesday" 23 | groups: 24 | kubernetes: 25 | patterns: [ "k8s.io/*" ] 26 | ignore: 27 | # Ignore controller-runtime, and Kubernetes major and minor updates. These should be done manually. 28 | - dependency-name: "sigs.k8s.io/controller-runtime" 29 | update-types: [ "version-update:semver-major", "version-update:semver-minor" ] 30 | - dependency-name: "k8s.io/*" 31 | update-types: [ "version-update:semver-major", "version-update:semver-minor" ] 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "37 4 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-24.04 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ go ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /pkg/logging/logging_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package logging 18 | 19 | import ( 20 | "testing" 21 | 22 | g "github.com/onsi/ginkgo/v2" 23 | o "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestConfig(t *testing.T) { 27 | o.RegisterFailHandler(g.Fail) 28 | g.RunSpecs(t, "Logging Suite") 29 | } 30 | 31 | var _ = g.BeforeSuite(func() { 32 | }) 33 | 34 | var _ = g.AfterSuite(func() { 35 | }) 36 | -------------------------------------------------------------------------------- /images/image_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2025 sriov-cni authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | 19 | set -x 20 | 21 | OCI_RUNTIME=$1 22 | IMAGE_UNDER_TEST=$2 23 | 24 | OUTPUT_DIR=$(mktemp -d) 25 | 26 | "${OCI_RUNTIME}" run -v "${OUTPUT_DIR}:/out" "${IMAGE_UNDER_TEST}" --cni-bin-dir=/out --no-sleep 27 | 28 | if [ ! -e "${OUTPUT_DIR}/sriov" ]; then 29 | echo "Output file ${OUTPUT_DIR}/sriov not found" 30 | exit 1 31 | fi 32 | 33 | if [ ! -s "${OUTPUT_DIR}/sriov" ]; then 34 | echo "Output file ${OUTPUT_DIR}/sriov is empty" 35 | exit 1 36 | fi 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /pkg/utils/utils_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo/v2" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestUtils(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Utils Suite") 29 | } 30 | 31 | var _ = BeforeSuite(func() { 32 | // create test sys tree 33 | err := CreateTmpSysFs() 34 | Expect(err).Should(Succeed()) 35 | }) 36 | 37 | var _ = AfterSuite(func() { 38 | err := RemoveTmpSysFs() 39 | Expect(err).Should(Succeed()) 40 | }) 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 sriov-cni authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | 17 | FROM golang:1.24-alpine as builder 18 | 19 | COPY . /usr/src/sriov-cni 20 | 21 | ENV HTTP_PROXY $http_proxy 22 | ENV HTTPS_PROXY $https_proxy 23 | 24 | WORKDIR /usr/src/sriov-cni 25 | RUN apk add --no-cache --virtual build-dependencies build-base=~0.5 && \ 26 | make clean && \ 27 | make build 28 | 29 | FROM alpine:3 30 | COPY --from=builder /usr/src/sriov-cni/build/sriov /usr/bin/ 31 | WORKDIR / 32 | 33 | LABEL io.k8s.display-name="SR-IOV CNI" 34 | 35 | COPY ./images/entrypoint.sh / 36 | 37 | ENTRYPOINT ["/entrypoint.sh"] 38 | -------------------------------------------------------------------------------- /Developer.md: -------------------------------------------------------------------------------- 1 | # Developer Readme 2 | 3 | * [Using Mockery](#using-mockery) 4 | 5 | ## Using Mockery 6 | 7 | Mockery (https://github.com/vektra/mockery) is used to auto-generate mock files for golang interfaces. The advantage of using Mockery is that there will be no need to manually write boilerplate code for mocking interfaces. 8 | 9 | Reading the readme file in Mockery is recommended to understand how to get started with Mockery. 10 | 11 | For each package, there may be a "mocks" folder with mock files generated by Mockery. To generate the mock for a particular interface the following command format should be used at the root project directory: 12 | 13 | ``` 14 | docker run -v "$PWD":/src -w /src vektra/mockery --recursive=true --name= --output=./pkg//mocks/ --filename=_mock.go --exported 15 | ``` 16 | 17 | An example for mocking the "pciUtils" interface in the "sriov" package is as follows: 18 | 19 | ``` 20 | docker run -v "$PWD":/src -w /src vektra/mockery --recursive=true --name=pciUtils --output=./pkg/sriov/mocks/ --filename=pci_utils_mock.go --exported 21 | ``` 22 | 23 | This will create the "mocks" directory if not present and will auto-generate the mock file "pci_utils_mock.go" for the "pciUtils" interface. 24 | -------------------------------------------------------------------------------- /pkg/config/config_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package config 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo/v2" 23 | . "github.com/onsi/gomega" 24 | 25 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils" 26 | ) 27 | 28 | func TestConfig(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Config Suite") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | // create test sys tree 35 | err := utils.CreateTmpSysFs() 36 | Expect(err).Should(Succeed()) 37 | }) 38 | 39 | var _ = AfterSuite(func() { 40 | err := utils.RemoveTmpSysFs() 41 | Expect(err).Should(Succeed()) 42 | }) 43 | -------------------------------------------------------------------------------- /pkg/sriov/sriov_suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package sriov 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | 23 | "testing" 24 | 25 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils" 26 | ) 27 | 28 | func TestConfig(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Sriov Suite") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | // create test sys tree 35 | err := utils.CreateTmpSysFs() 36 | Expect(err).Should(Succeed()) 37 | }) 38 | 39 | var _ = AfterSuite(func() { 40 | err := utils.RemoveTmpSysFs() 41 | Expect(err).Should(Succeed()) 42 | }) 43 | -------------------------------------------------------------------------------- /.github/workflows/image-push-master.yml: -------------------------------------------------------------------------------- 1 | name: "Push images on merge to master" 2 | 3 | env: 4 | IMAGE_NAME: ghcr.io/${{ github.repository }} 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | jobs: 11 | build-and-push-image-master: 12 | name: image build and push 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - name: Login to Docker 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.repository_owner }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Docker meta 32 | id: meta 33 | uses: docker/metadata-action@v5 34 | with: 35 | images: ${{ env.IMAGE_NAME }} 36 | 37 | - name: Build and push sriov-cni 38 | uses: docker/build-push-action@v6 39 | with: 40 | context: . 41 | push: true 42 | platforms: linux/amd64,linux/arm64,linux/ppc64le 43 | tags: | 44 | ${{ env.IMAGE_NAME }}:latest 45 | ${{ env.IMAGE_NAME }}:${{ github.sha }} 46 | labels: | 47 | ${{ steps.meta.outputs.labels }} 48 | file: ./Dockerfile 49 | -------------------------------------------------------------------------------- /cmd/sriov/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package main 18 | 19 | import ( 20 | "runtime" 21 | 22 | "github.com/containernetworking/cni/pkg/skel" 23 | "github.com/containernetworking/cni/pkg/version" 24 | 25 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/cnicommands" 26 | ) 27 | 28 | func init() { 29 | // this ensures that main runs only on main thread (thread group leader). 30 | // since namespace ops (unshare, setns) are done for a single thread, we 31 | // must ensure that the goroutine does not jump from OS thread to thread 32 | runtime.LockOSThread() 33 | } 34 | 35 | func main() { 36 | cniFuncs := skel.CNIFuncs{ 37 | Add: cnicommands.CmdAdd, 38 | Del: cnicommands.CmdDel, 39 | Check: cnicommands.CmdCheck, 40 | } 41 | skel.PluginMainFuncs(cniFuncs, version.All, "") 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/image-push-release.yml: -------------------------------------------------------------------------------- 1 | name: "Push images on release" 2 | 3 | env: 4 | IMAGE_NAME: ghcr.io/${{ github.repository }} 5 | 6 | on: 7 | push: 8 | tags: 9 | - v* 10 | jobs: 11 | build-and-push-image-release: 12 | runs-on: ubuntu-24.04 13 | name: image build and push on release 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - name: Login to Docker 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.repository_owner }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Docker meta 32 | id: docker_meta 33 | uses: docker/metadata-action@v5 34 | with: 35 | images: ${{ env.IMAGE_NAME }} 36 | flavor: | 37 | latest=false 38 | tags: | 39 | type=ref,event=tag 40 | 41 | - name: Build and push sriov-cni 42 | uses: docker/build-push-action@v6 43 | with: 44 | context: . 45 | push: true 46 | platforms: linux/amd64,linux/arm64,linux/ppc64le 47 | tags: | 48 | ${{ steps.docker_meta.outputs.tags }} 49 | labels: | 50 | ${{ steps.meta.outputs.labels }} 51 | file: ./Dockerfile 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/k8snetworkplumbingwg/sriov-cni 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/containernetworking/cni v1.3.0 7 | github.com/containernetworking/plugins v1.8.1-0.20251002142623-372953dfb89f 8 | github.com/k8snetworkplumbingwg/cni-log v0.0.0-20230801160229-b6e062c9e0f2 9 | github.com/onsi/ginkgo/v2 v2.25.2 10 | github.com/onsi/gomega v1.38.2 11 | github.com/stretchr/testify v1.10.0 12 | github.com/vishvananda/netlink v1.3.1 13 | golang.org/x/net v0.46.0 14 | golang.org/x/sys v0.37.0 15 | ) 16 | 17 | require ( 18 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 19 | github.com/coreos/go-iptables v0.8.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/go-logr/logr v1.4.3 // indirect 22 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 23 | github.com/google/go-cmp v0.7.0 // indirect 24 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/safchain/ethtool v0.6.2 // indirect 28 | github.com/stretchr/objx v0.5.2 // indirect 29 | github.com/vishvananda/netns v0.0.5 // indirect 30 | go.uber.org/automaxprocs v1.6.0 // indirect 31 | go.yaml.in/yaml/v3 v3.0.4 // indirect 32 | golang.org/x/text v0.30.0 // indirect 33 | golang.org/x/tools v0.37.0 // indirect 34 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 35 | gopkg.in/yaml.v2 v2.4.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | sigs.k8s.io/knftables v0.0.18 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug with SR-IOV CNI 4 | 5 | --- 6 | 7 | 8 | ### What happened? 9 | 10 | ### What did you expect to happen? 11 | 12 | ### What are the minimal steps needed to reproduce the bug? 13 | 14 | ### Anything else we need to know? 15 | 16 | ### Component Versions 17 | Please fill in the below table with the version numbers of applicable components used. 18 | 19 | Component | Version| 20 | ------------------------------|--------------------| 21 | |SR-IOV CNI Plugin || 22 | |Multus || 23 | |SR-IOV Network Device Plugin || 24 | |Kubernetes || 25 | |OS || 26 | 27 | ### Config Files 28 | Config file locations may be config dependent. 29 | 30 | ##### CNI config (Try '/etc/cni/net.d/') 31 | 32 | ##### Device pool config file location (Try '/etc/pcidp/config.json') 33 | 34 | ##### Multus config (Try '/etc/cni/multus/net.d') 35 | 36 | ##### Kubernetes deployment type ( Bare Metal, Kubeadm etc.) 37 | 38 | ##### Kubeconfig file 39 | 40 | ##### SR-IOV Network Custom Resource Definition 41 | 42 | 43 | ### Logs 44 | ##### SR-IOV Network Device Plugin Logs (use `kubectl logs $PODNAME`) 45 | 46 | ##### Multus logs (If enabled. Try '/var/log/multus.log' ) 47 | 48 | ##### Kubelet logs (journalctl -u kubelet) 49 | -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | 3 | This folder contains tests and related tools to run integration test suite. These tests leverages a mocked version of the cni, which runs with a 4 | fake, programmed version of the `/sys` filesystem and a mocked version of `NetlinkLib`. Both are implemented in `pkg/utils/testing.go`. 5 | 6 | The following diagram describes the interactions between the components. 7 | 8 | ```mermaid 9 | graph TD 10 | 11 | 12 | subgraph "pkg/utils/testing.go" 13 | CreateTmpSysFs 14 | MockNetlinkLib 15 | end 16 | 17 | sriovmocked["test/integration/sriov_mocked.go"] 18 | subgraph "sriov CNI" 19 | 20 | sriov["cmd/sriov/main.go"] 21 | cnicommands_pkg["CmdAdd | CmdDel"] 22 | sriov --- cnicommands_pkg 23 | 24 | end 25 | 26 | sriovmocked --- cnicommands_pkg 27 | sriovmocked -.setup.- CreateTmpSysFs 28 | sriovmocked -.setup.- MockNetlinkLib 29 | 30 | subgraph "System" 31 | calls_file[(/tmp/x/< pf_name >.calls)] 32 | PF{{PF << dummy >>}} 33 | VF1{{VF1 << dummy >>}} 34 | VF2{{VF2 << dummy >>}} 35 | end 36 | 37 | test_sriov_cni.sh 38 | 39 | test_sriov_cni.sh --> sriovmocked 40 | 41 | cnicommands_pkg --> CreateTmpSysFs 42 | cnicommands_pkg --> MockNetlinkLib 43 | 44 | MockNetlinkLib -.write.- calls_file 45 | MockNetlinkLib -..- PF 46 | MockNetlinkLib -..- VF1 47 | MockNetlinkLib -..- VF2 48 | 49 | test_sriov_cni.sh -.read.- calls_file 50 | 51 | linkStyle default stroke-width:2px 52 | linkStyle 1,4,5,6 stroke:green,stroke-width:4px 53 | ``` 54 | 55 | Test cases in this directory are based on the https://github.com/pgrange/bash_unit framework. 56 | -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | ## Dockerfile build 2 | 3 | This is used for distribution of SR-IOV CNI binary in a Docker image. 4 | 5 | Typically you'd build this from the root of your SR-IOV CNI clone, and you'd set the `-f` flag to specify the Dockerfile during build time. This allows the addition of the entirety of the SR-IOV CNI git clone as part of the Docker context. Use the `-f` flag with the root of the clone as the context (e.g. your current work directory would be root of git clone), such as: 6 | 7 | ``` 8 | $ docker build -t sriov-cni -f ./Dockerfile . 9 | ``` 10 | 11 | A `build_docker.sh` script is available for building the SR-IOV CNI docker image from the `./images` directory. 12 | 13 | --- 14 | 15 | ## Daemonset deployment 16 | 17 | You may wish to deploy SR-IOV CNI as a daemonset, you can do so by starting with the example Daemonset shown here: 18 | 19 | ``` 20 | $ kubectl create -f ./images/sriov-cni-daemonset.yaml 21 | ``` 22 | 23 | Note: The likely best practice here is to build your own image given the Dockerfile, and then push it to your preferred registry, and change the `image` fields in the Daemonset YAML to reference that image. 24 | 25 | --- 26 | 27 | ### Development notes 28 | 29 | Example docker run command: 30 | 31 | ``` 32 | $ docker run -it -v /opt/cni/bin/:/host/opt/cni/bin/ --entrypoint=/bin/bash ghcr.io/k8snetworkplumbingwg/sriov-cni 33 | ``` 34 | 35 | Originally inspired by and is a portmanteau of the [Flannel daemonset](https://github.com/coreos/flannel/blob/master/Documentation/kube-flannel.yml), the [Calico Daemonset](https://github.com/projectcalico/calico/blob/master/v2.0/getting-started/kubernetes/installation/hosted/k8s-backend-addon-manager/calico-daemonset.yaml), and the [Calico CNI install bash script](https://github.com/projectcalico/cni-plugin/blob/be4df4db2e47aa7378b1bdf6933724bac1f348d0/k8s-install/scripts/install-cni.sh#L104-L153). 36 | -------------------------------------------------------------------------------- /test/integration/sriov_mocked.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "runtime" 22 | 23 | "github.com/containernetworking/cni/pkg/skel" 24 | "github.com/containernetworking/cni/pkg/version" 25 | 26 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/cnicommands" 27 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/config" 28 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils" 29 | ) 30 | 31 | func init() { 32 | // this ensures that main runs only on main thread (thread group leader). 33 | // since namespace ops (unshare, setns) are done for a single thread, we 34 | // must ensure that the goroutine does not jump from OS thread to thread 35 | runtime.LockOSThread() 36 | } 37 | 38 | func main() { 39 | customCNIDir, ok := os.LookupEnv("DEFAULT_CNI_DIR") 40 | if ok { 41 | config.DefaultCNIDir = customCNIDir 42 | } 43 | 44 | err := utils.CreateTmpSysFs() 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | defer func() { 50 | err := utils.RemoveTmpSysFs() 51 | if err != nil { 52 | panic(err) 53 | } 54 | }() 55 | 56 | cancel := utils.MockNetlinkLib(config.DefaultCNIDir) 57 | defer cancel() 58 | 59 | cniFuncs := skel.CNIFuncs{ 60 | Add: cnicommands.CmdAdd, 61 | Del: cnicommands.CmdDel, 62 | Check: cnicommands.CmdCheck, 63 | } 64 | skel.PluginMainFuncs(cniFuncs, version.All, "") 65 | } 66 | -------------------------------------------------------------------------------- /images/sriov-cni-daemonset.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 sriov-cni authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | 17 | --- 18 | apiVersion: apps/v1 19 | kind: DaemonSet 20 | metadata: 21 | name: kube-sriov-cni-ds 22 | namespace: kube-system 23 | labels: 24 | tier: node 25 | app: sriov-cni 26 | spec: 27 | selector: 28 | matchLabels: 29 | name: sriov-cni 30 | template: 31 | metadata: 32 | labels: 33 | name: sriov-cni 34 | tier: node 35 | app: sriov-cni 36 | spec: 37 | tolerations: 38 | - key: node-role.kubernetes.io/master 39 | operator: Exists 40 | effect: NoSchedule 41 | - key: node-role.kubernetes.io/control-plane 42 | operator: Exists 43 | effect: NoSchedule 44 | containers: 45 | - name: kube-sriov-cni 46 | image: ghcr.io/k8snetworkplumbingwg/sriov-cni 47 | imagePullPolicy: IfNotPresent 48 | securityContext: 49 | allowPrivilegeEscalation: false 50 | privileged: false 51 | readOnlyRootFilesystem: true 52 | capabilities: 53 | drop: 54 | - ALL 55 | resources: 56 | requests: 57 | cpu: "100m" 58 | memory: "50Mi" 59 | limits: 60 | cpu: "100m" 61 | memory: "50Mi" 62 | volumeMounts: 63 | - name: cnibin 64 | mountPath: /host/opt/cni/bin 65 | volumes: 66 | - name: cnibin 67 | hostPath: 68 | path: /opt/cni/bin 69 | -------------------------------------------------------------------------------- /pkg/utils/mocks/pci_allocator_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.14.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // PCIAllocation is an autogenerated mock type for the PCIAllocation type 8 | type PCIAllocation struct { 9 | mock.Mock 10 | } 11 | 12 | // CleanAllocatedPCI provides a mock function with given fields: _a0 13 | func (_m *PCIAllocation) CleanAllocatedPCI(_a0 string) error { 14 | ret := _m.Called(_a0) 15 | 16 | var r0 error 17 | if rf, ok := ret.Get(0).(func(string) error); ok { 18 | r0 = rf(_a0) 19 | } else { 20 | r0 = ret.Error(0) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // IsAllocated provides a mock function with given fields: _a0 27 | func (_m *PCIAllocation) IsAllocated(_a0 string) (bool, string, error) { 28 | ret := _m.Called(_a0) 29 | 30 | var r0 bool 31 | if rf, ok := ret.Get(0).(func(string) bool); ok { 32 | r0 = rf(_a0) 33 | } else { 34 | r0 = ret.Get(0).(bool) 35 | } 36 | 37 | var r1 string 38 | if rf, ok := ret.Get(1).(func(string) string); ok { 39 | r1 = rf(_a0) 40 | } else { 41 | r1 = ret.Get(1).(string) 42 | } 43 | 44 | var r2 error 45 | if rf, ok := ret.Get(2).(func(string) error); ok { 46 | r2 = rf(_a0) 47 | } else { 48 | r2 = ret.Error(2) 49 | } 50 | 51 | return r0, r1, r2 52 | } 53 | 54 | // SaveAllocatedPCI provides a mock function with given fields: _a0, _a1 55 | func (_m *PCIAllocation) SaveAllocatedPCI(_a0 string, _a1 string) error { 56 | ret := _m.Called(_a0, _a1) 57 | 58 | var r0 error 59 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 60 | r0 = rf(_a0, _a1) 61 | } else { 62 | r0 = ret.Error(0) 63 | } 64 | 65 | return r0 66 | } 67 | 68 | type mockConstructorTestingTNewPCIAllocation interface { 69 | mock.TestingT 70 | Cleanup(func()) 71 | } 72 | 73 | // NewPCIAllocation creates a new instance of PCIAllocation. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 74 | func NewPCIAllocation(t mockConstructorTestingTNewPCIAllocation) *PCIAllocation { 75 | mock := &PCIAllocation{} 76 | mock.Mock.Test(t) 77 | 78 | t.Cleanup(func() { mock.AssertExpectations(t) }) 79 | 80 | return mock 81 | } 82 | -------------------------------------------------------------------------------- /pkg/utils/packet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "sync/atomic" 21 | "time" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | 26 | "github.com/stretchr/testify/mock" 27 | "github.com/vishvananda/netlink" 28 | "golang.org/x/sys/unix" 29 | 30 | mocks_utils "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils/mocks" 31 | ) 32 | 33 | var _ = Describe("Packets", func() { 34 | 35 | Context("WaitForCarrier", func() { 36 | It("should wait until the link has IFF_UP flag", func() { 37 | DeferCleanup(func(old NetlinkManager) { netLinkLib = old }, netLinkLib) 38 | 39 | mockedNetLink := &mocks_utils.NetlinkManager{} 40 | netLinkLib = mockedNetLink 41 | 42 | rawFlagsAtomic := new(uint32) 43 | *rawFlagsAtomic = unix.IFF_UP 44 | 45 | fakeLink := &FakeLink{LinkAttrs: netlink.LinkAttrs{ 46 | Index: 1000, 47 | Name: "dummylink", 48 | RawFlags: atomic.LoadUint32(rawFlagsAtomic), 49 | }} 50 | 51 | mockedNetLink.On("LinkByName", "dummylink").Return(fakeLink, nil).Run(func(_ mock.Arguments) { 52 | fakeLink.RawFlags = atomic.LoadUint32(rawFlagsAtomic) 53 | }) 54 | 55 | hasCarrier := make(chan bool) 56 | go func() { 57 | hasCarrier <- WaitForCarrier("dummylink", 5*time.Second) 58 | }() 59 | 60 | Consistently(hasCarrier, "100ms").ShouldNot(Receive()) 61 | 62 | go func() { 63 | atomic.StoreUint32(rawFlagsAtomic, unix.IFF_UP|unix.IFF_RUNNING) 64 | }() 65 | 66 | Eventually(hasCarrier, "300ms").Should(Receive()) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /pkg/utils/pci_allocator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | 23 | "github.com/containernetworking/plugins/pkg/ns" 24 | "github.com/containernetworking/plugins/pkg/testutils" 25 | ) 26 | 27 | var _ = Describe("PCIAllocator", func() { 28 | var targetNetNS ns.NetNS 29 | var err error 30 | 31 | AfterEach(func() { 32 | if targetNetNS != nil { 33 | targetNetNS.Close() 34 | err = testutils.UnmountNS(targetNetNS) 35 | } 36 | }) 37 | 38 | Context("IsAllocated", func() { 39 | It("Assuming is not allocated", func() { 40 | allocator := NewPCIAllocator(ts.dirRoot) 41 | isAllocated, err := allocator.IsAllocated("0000:af:00.1") 42 | Expect(err).ToNot(HaveOccurred()) 43 | Expect(isAllocated).To(BeFalse()) 44 | }) 45 | 46 | It("Assuming is allocated and namespace exist", func() { 47 | targetNetNS, err = testutils.NewNS() 48 | Expect(err).NotTo(HaveOccurred()) 49 | allocator := NewPCIAllocator(ts.dirRoot) 50 | 51 | err = allocator.SaveAllocatedPCI("0000:af:00.1", targetNetNS.Path()) 52 | Expect(err).ToNot(HaveOccurred()) 53 | 54 | isAllocated, err := allocator.IsAllocated("0000:af:00.1") 55 | Expect(err).ToNot(HaveOccurred()) 56 | Expect(isAllocated).To(BeTrue()) 57 | }) 58 | 59 | It("Assuming is allocated and namespace doesn't exist", func() { 60 | targetNetNS, err = testutils.NewNS() 61 | Expect(err).NotTo(HaveOccurred()) 62 | 63 | allocator := NewPCIAllocator(ts.dirRoot) 64 | err = allocator.SaveAllocatedPCI("0000:af:00.1", targetNetNS.Path()) 65 | Expect(err).ToNot(HaveOccurred()) 66 | err = targetNetNS.Close() 67 | Expect(err).ToNot(HaveOccurred()) 68 | err = testutils.UnmountNS(targetNetNS) 69 | Expect(err).ToNot(HaveOccurred()) 70 | 71 | isAllocated, err := allocator.IsAllocated("0000:af:00.1") 72 | Expect(err).ToNot(HaveOccurred()) 73 | Expect(isAllocated).To(BeFalse()) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gopath/* 2 | /build/* 3 | /bin/* 4 | /test/coverage* 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/go,macos,linux,windows,visualstudiocode 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,macos,linux,windows,visualstudiocode 8 | 9 | ### Go ### 10 | # Binaries for programs and plugins 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | 17 | # Test binary, built with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # Dependency directories (remove the comment below to include it) 24 | # vendor/ 25 | 26 | ### Go Patch ### 27 | /vendor/ 28 | /Godeps/ 29 | 30 | ### Linux ### 31 | *~ 32 | 33 | # temporary files which can be created if a process still has a handle open of a deleted file 34 | .fuse_hidden* 35 | 36 | # KDE directory preferences 37 | .directory 38 | 39 | # Linux trash folder which might appear on any partition or disk 40 | .Trash-* 41 | 42 | # .nfs files are created when an open file is removed but is still being accessed 43 | .nfs* 44 | 45 | ### macOS ### 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | 55 | # Thumbnails 56 | ._* 57 | 58 | # Files that might appear in the root of a volume 59 | .DocumentRevisions-V100 60 | .fseventsd 61 | .Spotlight-V100 62 | .TemporaryItems 63 | .Trashes 64 | .VolumeIcon.icns 65 | .com.apple.timemachine.donotpresent 66 | 67 | # Directories potentially created on remote AFP share 68 | .AppleDB 69 | .AppleDesktop 70 | Network Trash Folder 71 | Temporary Items 72 | .apdisk 73 | 74 | ### VisualStudioCode ### 75 | .vscode/* 76 | !.vscode/settings.json 77 | !.vscode/tasks.json 78 | !.vscode/launch.json 79 | !.vscode/extensions.json 80 | *.code-workspace 81 | 82 | # Local History for Visual Studio Code 83 | .history/ 84 | 85 | ### IntelliJ ### 86 | .idea 87 | 88 | ### VisualStudioCode Patch ### 89 | # Ignore all local history of files 90 | .history 91 | .ionide 92 | 93 | ### Windows ### 94 | # Windows thumbnail cache files 95 | Thumbs.db 96 | Thumbs.db:encryptable 97 | ehthumbs.db 98 | ehthumbs_vista.db 99 | 100 | # Dump file 101 | *.stackdump 102 | 103 | # Folder config file 104 | [Dd]esktop.ini 105 | 106 | # Recycle Bin used on file shares 107 | $RECYCLE.BIN/ 108 | 109 | # Windows Installer files 110 | *.cab 111 | *.msi 112 | *.msix 113 | *.msm 114 | *.msp 115 | 116 | # Windows shortcuts 117 | *.lnk 118 | 119 | # End of https://www.toptal.com/developers/gitignore/api/go,macos,linux,windows,visualstudiocode 120 | -------------------------------------------------------------------------------- /test/integration/libtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2025 sriov-cni authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | 19 | this_folder="$(dirname "$(readlink --canonicalize "${BASH_SOURCE[0]}")")" 20 | export CNI_PATH="${this_folder}/test_utils" 21 | export CNI_CONTAINERID=stub_container 22 | 23 | setup() { 24 | ip netns del test_root_ns 2> /dev/null || true 25 | ip netns add test_root_ns 26 | 27 | # See pkg/utils/testing.go 28 | ip netns exec test_root_ns ip link add enp175s0f1 type dummy 29 | ip netns exec test_root_ns ip link add enp175s6 type dummy 30 | ip netns exec test_root_ns ip link add enp175s7 type dummy 31 | 32 | DEFAULT_CNI_DIR=$(mktemp -d "${this_folder}/tmp/default_cni_dir.XXXXX") 33 | export DEFAULT_CNI_DIR 34 | } 35 | 36 | teardown() { 37 | if [ -n "${INT_TEST_SKIP_CLEANUP}" ]; then 38 | return 39 | fi 40 | 41 | # Double check the variable points to something created by the setup() function. 42 | if [[ $DEFAULT_CNI_DIR == *"tmp/default_cni_dir."* ]]; then 43 | rm -rf "$DEFAULT_CNI_DIR" 44 | fi 45 | } 46 | 47 | assert_file_does_not_exists() { 48 | file=$1 49 | if [ -f "$file" ]; then 50 | fail "File [$file] exists" 51 | fi 52 | } 53 | 54 | invoke_sriov_cni() { 55 | echo "$CNI_INPUT" | ip netns exec test_root_ns go run -cover -covermode atomic "${this_folder}/sriov_mocked.go" 56 | } 57 | 58 | create_network_ns() { 59 | name=$1 60 | delete_network_ns "$name" 61 | 62 | ip netns add "${name}" 63 | 64 | export CNI_NETNS=/run/netns/${name} 65 | } 66 | 67 | delete_network_ns() { 68 | name=$1 69 | ip netns del "${name}" 2>/dev/null 70 | } 71 | 72 | assert_file_contains() { 73 | file=$1 74 | substr=$2 75 | if ! grep -q "$substr" "$file"; then 76 | fail "File [$file] does not contains [$substr], contents: \n $(cat "$file")" 77 | fi 78 | } 79 | 80 | wait_for_file_to_exist() { 81 | file=$1 82 | 83 | SECONDS=0 84 | until [ -f "$file" ] 85 | do 86 | sleep 1 87 | if [[ $SECONDS -gt 20 ]]; then 88 | fail "File [$file] does not exists after [$SECONDS] seconds." 89 | fi 90 | done 91 | } 92 | -------------------------------------------------------------------------------- /images/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2025 sriov-cni authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | 19 | # Always exit on errors. 20 | set -e 21 | 22 | # Set known directories. 23 | CNI_BIN_DIR="/host/opt/cni/bin" 24 | SRIOV_BIN_FILE="/usr/bin/sriov" 25 | NO_SLEEP=0 26 | 27 | # Give help text for parameters. 28 | usage() 29 | { 30 | printf "This is an entrypoint script for SR-IOV CNI to overlay its\n" 31 | printf "binary into location in a filesystem. The binary file will\n" 32 | printf "be copied to the corresponding directory.\n" 33 | printf "\n" 34 | printf "./entrypoint.sh\n" 35 | printf "\t-h --help\n" 36 | printf "\t--cni-bin-dir=%s\n" "$CNI_BIN_DIR" 37 | printf "\t--sriov-bin-file=%s\n" "$SRIOV_BIN_FILE" 38 | printf "\t--no-sleep\n" 39 | } 40 | 41 | # Parse parameters given as arguments to this script. 42 | while [ "$1" != "" ]; do 43 | PARAM=$(echo "$1" | awk -F= '{print $1}') 44 | VALUE=$(echo "$1" | awk -F= '{print $2}') 45 | case $PARAM in 46 | -h | --help) 47 | usage 48 | exit 49 | ;; 50 | --cni-bin-dir) 51 | CNI_BIN_DIR=$VALUE 52 | ;; 53 | --sriov-bin-file) 54 | SRIOV_BIN_FILE=$VALUE 55 | ;; 56 | --no-sleep) 57 | NO_SLEEP=1 58 | ;; 59 | *) 60 | /bin/echo "ERROR: unknown parameter \"$PARAM\"" 61 | usage 62 | exit 1 63 | ;; 64 | esac 65 | shift 66 | done 67 | 68 | 69 | # Loop through and verify each location each. 70 | for i in $CNI_BIN_DIR $SRIOV_BIN_FILE 71 | do 72 | if [ ! -e "$i" ]; then 73 | /bin/echo "Location $i does not exist" 74 | exit 1; 75 | fi 76 | done 77 | 78 | # Copy file into proper place. 79 | cp -f "$SRIOV_BIN_FILE" "$CNI_BIN_DIR" 80 | 81 | if [ $NO_SLEEP -eq 1 ]; then 82 | exit 0 83 | fi 84 | 85 | echo "Entering sleep... (success)" 86 | trap : TERM INT 87 | 88 | # Sleep forever. 89 | # sleep infinity is not available in alpine; instead lets go sleep for ~68 years. Hopefully that's enough sleep 90 | sleep 2147483647 & wait 91 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 3m 3 | 4 | linters-settings: 5 | dupl: 6 | threshold: 100 7 | funlen: 8 | lines: 100 9 | statements: 50 10 | goconst: 11 | min-len: 2 12 | min-occurrences: 2 13 | gocritic: 14 | enabled-tags: 15 | - diagnostic 16 | - experimental 17 | - opinionated 18 | - performance 19 | - style 20 | disabled-checks: 21 | - dupImport # https://github.com/go-critic/go-critic/issues/845 22 | - ifElseChain 23 | - octalLiteral 24 | - whyNoLint 25 | - wrapperFunc 26 | - unnamedResult 27 | settings: 28 | hugeParam: 29 | sizeThreshold: 512 30 | rangeValCopy: 31 | sizeThreshold: 512 32 | gocyclo: 33 | min-complexity: 15 34 | goimports: 35 | local-prefixes: github.com/k8snetworkplumbingwg/sriov-cni 36 | gomnd: 37 | settings: 38 | mnd: 39 | # don't include the "operation" and "assign" 40 | checks: argument,case,condition,return 41 | lll: 42 | line-length: 140 43 | misspell: 44 | locale: US 45 | prealloc: 46 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 47 | # True by default. 48 | simple: true 49 | range-loops: true # Report preallocation suggestions on range loops, true by default 50 | for-loops: false # Report preallocation suggestions on for loops, false by default 51 | 52 | linters: 53 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 54 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 55 | disable-all: true 56 | enable: 57 | - bodyclose 58 | - dogsled 59 | - dupl 60 | - errcheck 61 | - copyloopvar 62 | - exhaustive 63 | # - funlen 64 | - goconst 65 | - gocritic 66 | # - gocyclo 67 | - gofmt 68 | - goimports 69 | # - mnd 70 | - goprintffuncname 71 | - gosec 72 | - gosimple 73 | - govet 74 | - ineffassign 75 | # - lll 76 | - misspell 77 | - nakedret 78 | - prealloc 79 | - rowserrcheck 80 | - staticcheck 81 | - stylecheck 82 | - typecheck 83 | - unconvert 84 | - unparam 85 | - unused 86 | - whitespace 87 | 88 | issues: 89 | # Excluding configuration per-path, per-linter, per-text and per-source 90 | exclude-rules: 91 | - path: _test\.go 92 | linters: 93 | - gomnd 94 | - gosec 95 | - dupl 96 | - lll 97 | - stylecheck 98 | - goconst 99 | exclude-dirs: 100 | - .github/ 101 | - docs/ 102 | - images/ 103 | -------------------------------------------------------------------------------- /make/license.mk: -------------------------------------------------------------------------------- 1 | # This file contains makefile targets to update copyrights and third party notices in the repo 2 | 3 | # --- Configurable license header settings --- 4 | COPYRIGHT_YEAR ?= $(shell date +%Y) 5 | COPYRIGHT_OWNER ?= NVIDIA CORPORATION & AFFILIATES 6 | COPYRIGHT_STYLE ?= apache 7 | COPYRIGHT_FLAGS ?= -s 8 | COPYRIGHT_EXCLUDE ?= vendor deployment config bundle .* 9 | GIT_LS_FILES_EXCLUDES := $(foreach d,$(COPYRIGHT_EXCLUDE),:^"$(d)") 10 | 11 | # --- Tool paths --- 12 | BIN_DIR ?= ./bin 13 | ADDLICENSE ?= $(BIN_DIR)/addlicense 14 | ADDLICENSE_VERSION ?= latest 15 | GO_LICENSES ?= $(BIN_DIR)/go-licenses 16 | GO_LICENSES_VERSION ?= latest 17 | 18 | # Ensure bin dir exists 19 | $(BIN_DIR): 20 | @mkdir -p $(BIN_DIR) 21 | 22 | # Install addlicense locally 23 | .PHONY: addlicense 24 | addlicense: $(BIN_DIR) 25 | @if [ ! -f "$(ADDLICENSE)" ]; then \ 26 | echo "Installing addlicense to $(ADDLICENSE)..."; \ 27 | GOBIN=$(abspath $(BIN_DIR)) go install github.com/google/addlicense@$(ADDLICENSE_VERSION); \ 28 | else \ 29 | echo "addlicense already installed at $(ADDLICENSE)"; \ 30 | fi 31 | 32 | # Check headers 33 | .PHONY: copyright-check 34 | copyright-check: addlicense 35 | @echo "Checking copyright headers..." 36 | @git ls-files '*' $(GIT_LS_FILES_EXCLUDES) | xargs grep -ILi "$(COPYRIGHT_OWNER)" | xargs -r $(ADDLICENSE) -check -c "$(COPYRIGHT_OWNER)" -l $(COPYRIGHT_STYLE) $(COPYRIGHT_FLAGS) -y $(COPYRIGHT_YEAR) 37 | 38 | # Fix headers 39 | .PHONY: copyright 40 | copyright: addlicense 41 | @echo "Adding copyright headers..." 42 | @git ls-files '*' $(GIT_LS_FILES_EXCLUDES) | xargs grep -ILi "$(COPYRIGHT_OWNER)" | xargs -r $(ADDLICENSE) -c "$(COPYRIGHT_OWNER)" -l $(COPYRIGHT_STYLE) $(COPYRIGHT_FLAGS) -y $(COPYRIGHT_YEAR) 43 | 44 | # Install go-licenses tool locally 45 | .PHONY: go-licenses 46 | go-licenses: $(BIN_DIR) 47 | @if [ ! -f "$(GO_LICENSES)" ]; then \ 48 | echo "Installing go-licenses to $(GO_LICENSES)..."; \ 49 | GOBIN=$(abspath $(BIN_DIR)) go install github.com/google/go-licenses@$(GO_LICENSES_VERSION); \ 50 | else \ 51 | echo "go-licenses already installed at $(GO_LICENSES)"; \ 52 | fi 53 | 54 | # Generate THIRD_PARTY_NOTICES from go-licenses 55 | .PHONY: third-party-licenses 56 | third-party-licenses: go-licenses 57 | @echo "Collecting third-party licenses..." 58 | @$(GO_LICENSES) save ./... --save_path=third_party_licenses --ignore=github.com/k8snetworkplumbingwg/cni-log 59 | @echo "Generating THIRD_PARTY_NOTICES..." 60 | @find third_party_licenses -type f -iname "LICENSE*" | sort --ignore-case | while read -r license; do \ 61 | echo "---"; \ 62 | echo "## $$(basename $$(dirname "$$license"))"; \ 63 | echo ""; \ 64 | cat "$$license"; \ 65 | echo ""; \ 66 | done > THIRD_PARTY_NOTICES 67 | @rm -rf third_party_licenses 68 | @echo "THIRD_PARTY_NOTICES updated." 69 | -------------------------------------------------------------------------------- /test/integration/test_sriov_cni.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2025 sriov-cni authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | 19 | # shellcheck source-path=test/integration 20 | . libtest.sh 21 | 22 | test_macaddress() { 23 | 24 | create_network_ns "container_1" 25 | 26 | export CNI_IFNAME=net1 27 | 28 | read -r -d '' CNI_INPUT <<- EOM 29 | { 30 | "type": "sriov", 31 | "cniVersion": "0.3.1", 32 | "name": "sriov-network", 33 | "ipam": { 34 | "type": "test-ipam-cni" 35 | }, 36 | "deviceID": "0000:af:06.0", 37 | "mac": "60:00:00:00:00:E1", 38 | "logFile": "${DEFAULT_CNI_DIR}/sriov.log", 39 | "logLevel": "debug" 40 | } 41 | EOM 42 | 43 | export CNI_COMMAND=ADD 44 | assert invoke_sriov_cni 45 | assert 'ip netns exec container_1 ip link | grep -i 60:00:00:00:00:E1' 46 | 47 | export CNI_COMMAND=DEL 48 | assert 'invoke_sriov_cni' 49 | assert 'ip netns exec test_root_ns ip link show enp175s6' 50 | } 51 | 52 | 53 | test_vlan() { 54 | 55 | create_network_ns "container_1" 56 | 57 | export CNI_IFNAME=net1 58 | 59 | read -r -d '' CNI_INPUT <<- EOM 60 | { 61 | "type": "sriov", 62 | "cniVersion": "0.3.1", 63 | "name": "sriov-network", 64 | "vlan": 1234, 65 | "ipam": { 66 | "type": "test-ipam-cni" 67 | }, 68 | "deviceID": "0000:af:06.0", 69 | "mac": "60:00:00:00:00:E1", 70 | "logLevel": "debug" 71 | } 72 | EOM 73 | 74 | export CNI_COMMAND=ADD 75 | assert invoke_sriov_cni 76 | assert_file_contains "${DEFAULT_CNI_DIR}/enp175s0f1.calls" "LinkSetVfVlanQosProto enp175s0f1 0 1234 0 33024" 77 | 78 | export CNI_COMMAND=DEL 79 | assert invoke_sriov_cni 80 | assert 'ip netns exec test_root_ns ip link show enp175s6' 81 | assert_file_contains "${DEFAULT_CNI_DIR}/enp175s0f1.calls" "LinkSetVfVlanQosProto enp175s0f1 0 0 0 33024" 82 | } 83 | 84 | 85 | test_mtu_reset() { 86 | 87 | create_network_ns "container_1" 88 | 89 | assert 'ip netns exec test_root_ns ip link set mtu 3333 dev enp175s6' 90 | 91 | export CNI_IFNAME=net1 92 | 93 | read -r -d '' CNI_INPUT <<- EOM 94 | { 95 | "type": "sriov", 96 | "cniVersion": "0.3.1", 97 | "name": "sriov-network", 98 | "vlan": 1234, 99 | "ipam": { 100 | "type": "test-ipam-cni" 101 | }, 102 | "deviceID": "0000:af:06.0", 103 | "mac": "60:00:00:00:00:E1", 104 | "logFile": "${DEFAULT_CNI_DIR}/sriov.log", 105 | "logLevel": "debug" 106 | } 107 | EOM 108 | 109 | export CNI_COMMAND=ADD 110 | assert invoke_sriov_cni 111 | 112 | # Verify the VF has the correct MTU inside the container 113 | assert 'ip netns exec container_1 ip link | grep -i 3333' 114 | 115 | # Simulate an application modifying the MTU value 116 | assert 'ip netns exec container_1 ip link set mtu 4444 dev net1' 117 | assert 'ip netns exec container_1 ip link | grep -i 4444' 118 | 119 | export CNI_COMMAND=DEL 120 | assert invoke_sriov_cni 121 | assert 'ip netns exec test_root_ns ip link show enp175s6 | grep 3333' 122 | } 123 | -------------------------------------------------------------------------------- /docs/configuration-reference.md: -------------------------------------------------------------------------------- 1 | ## Configuration reference - SR-IOV CNI 2 | 3 | The SR-IOV CNI configures networks through a CNI spec configuration object. In a Kubernetes cluster set up with Multus this object is most often delivered as a Network Attachment Definition. 4 | 5 | 6 | ### Parameters 7 | * `name` (string, required): the name of the network 8 | * `type` (string, required): "sriov" 9 | * `ipam` (dictionary, optional): IPAM configuration to be used for this network. 10 | * `deviceID` (string, required): A valid pci address of an SRIOV NIC's VF. e.g. "0000:03:02.3" 11 | * `vlan` (int, optional): VLAN ID to assign for the VF. Value must be in the range 0-4094 (0 for disabled, 1-4094 for valid VLAN IDs). 12 | * `vlanQoS` (int, optional): VLAN QoS to assign for the VF. Value must be in the range 0-7. This option requires `vlan` field to be set to a non-zero value. Otherwise, the error will be returned. 13 | * `vlanProto` (string, optional): VLAN protocol to assign for the VF. Allowed values: "802.1ad", "802.1q" (default). 14 | * `mac` (string, optional): MAC address to assign for the VF 15 | * `spoofchk` (string, optional): turn packet spoof checking on or off for the VF 16 | * `trust` (string, optional): turn trust setting on or off for the VF 17 | * `link_state` (string, optional): enforce link state for the VF. Allowed values: auto, enable, disable. Note that driver support may differ for this feature. For example, `i40e` is known to work but `igb` doesn't. 18 | * `min_tx_rate` (int, optional): change the allowed minimum transmit bandwidth, in Mbps, for the VF. Setting this to 0 disables rate limiting. The min_tx_rate value should be <= max_tx_rate. Support of this feature depends on NICs and drivers. 19 | * `max_tx_rate` (int, optional): change the allowed maximum transmit bandwidth, in Mbps, for the VF. 20 | Setting this to 0 disables rate limiting. 21 | * `logLevel` (string, optional): either of panic, error, warning, info, debug with a default of info. 22 | * `logFile` (string, optional): path to file for log output. By default, this will log to stderr. Logging to stderr 23 | means that the logs will show up in crio logs (in the journal in most configurations) and in multus pod logs. 24 | 25 | 26 | An SR-IOV CNI config with each field filled out looks like: 27 | 28 | ```json 29 | { 30 | "cniVersion": "0.3.1", 31 | "name": "sriov-dpdk", 32 | "type": "sriov", 33 | "deviceID": "0000:03:02.0", 34 | "mac": "CA:FE:C0:FF:EE:00", 35 | "vlan": 1000, 36 | "vlanQoS": 4, 37 | "vlanProto": "802.1ad", 38 | "min_tx_rate": 100, 39 | "max_tx_rate": 200, 40 | "spoofchk": "off", 41 | "trust": "on", 42 | "link_state": "enable", 43 | "logLevel": "debug", 44 | "logFile": "/tmp/sriov.log" 45 | } 46 | ``` 47 | 48 | ### Runtime Configuration 49 | 50 | The SR-IOV CNI accepts a MAC address when passed as a runtime configuration - that is as part of a Kubernetes Pod spec. An example pod with a runtime configuration is: 51 | 52 | ``` 53 | apiVersion: v1 54 | kind: Pod 55 | metadata: 56 | name: samplepod 57 | annotations: 58 | k8s.v1.cni.cncf.io/networks: '[ 59 | { 60 | "name": "sriov-net", 61 | "mac": "CA:FE:C0:FF:EE:00" 62 | } 63 | ]' 64 | spec: 65 | containers: 66 | - name: runTimeConfig 67 | command: ["/bin/bash", "-c", "sleep 300"] 68 | image: centos/tools 69 | 70 | ``` 71 | 72 | The above config will configure a VF of type "sriov-net" with the MAC address configured as the value supplied under the 'k8s.v1.cni.cncf.io/networks'. Where the MAC address supplied is invalid the container may be created with an unexpected address. 73 | 74 | To avoid this it's key to ensure the supplied MAC is valid for the specified interface. On some systems setting a Multicast MAC address (Where the least significant bit of the first octet is '1') results in failure to set the MAC address. 75 | -------------------------------------------------------------------------------- /.github/workflows/buildtest.yml: -------------------------------------------------------------------------------- 1 | #Originally from https://raw.githubusercontent.com/intel/multus-cni/master/.github/workflows/go-build-test-amd64.yml 2 | name: Go-build-and-test-amd64 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 8 * * 0" # every sunday 8 | jobs: 9 | build-test: 10 | strategy: 11 | matrix: 12 | go-version: [1.24.x] 13 | os: [ubuntu-24.04] 14 | goos: [linux] 15 | goarch: [amd64, arm64, ppc64le] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v4 25 | 26 | - name: Build test for ${{ matrix.goarch }} 27 | env: 28 | GOARCH: ${{ matrix.goarch }} 29 | GOOS: ${{ matrix.goos }} 30 | run: GOARCH="${TARGET}" make build 31 | 32 | - name: Go test 33 | if: ${{ matrix.goarch }} == "amd64" 34 | run: sudo make test-race # sudo needed for netns change in test 35 | 36 | - name: Integration test for ${{ matrix.goarch }} 37 | env: 38 | GOARCH: ${{ matrix.goarch }} 39 | GOOS: ${{ matrix.goos }} 40 | run: sudo FORCE_COLOR=true INT_TEST_SKIP_CLEANUP=true make test-integration 41 | 42 | - name: Prepare integration-test archive 43 | if: always() 44 | # `actions/upload-artifact` step need the files to be readable and files can't have Colon (:) 45 | run: | 46 | sudo chmod -R a+rwx test/integration/tmp/* 47 | find test/integration/tmp -name "*:*" -exec bash -c 'mv $0 ${0//:/_}' {} \; 48 | 49 | - uses: actions/upload-artifact@v4 50 | if: always() 51 | with: 52 | name: test-integration- ${{ matrix.goarch }} 53 | path: ./test/integration/tmp/ 54 | 55 | coverage: 56 | runs-on: ubuntu-24.04 57 | needs: build-test 58 | name: coverage 59 | steps: 60 | - name: Set up Go 61 | uses: actions/setup-go@v5 62 | with: 63 | go-version: 1.24.x 64 | 65 | - name: Check out code 66 | uses: actions/checkout@v4 67 | 68 | - name: Go test with coverage 69 | run: sudo make test-coverage test-integration merge-test-coverage 70 | 71 | - name: Coveralls 72 | uses: coverallsapp/github-action@v2 73 | with: 74 | github-token: ${{ secrets.GITHUB_TOKEN }} 75 | file: test/coverage/cover.out 76 | format: golang 77 | 78 | sriov-operator-e2e-test: 79 | name: SR-IOV operator e2e tests 80 | needs: [ build-test ] 81 | runs-on: [ sriov ] 82 | env: 83 | TEST_REPORT_PATH: k8s-artifacts 84 | steps: 85 | - name: Check out the repo 86 | uses: actions/checkout@v4 87 | 88 | - name: build sriov-cni image 89 | run: podman build -t ghaction-sriov-cni:pr-${{github.event.pull_request.number}} . 90 | 91 | - name: Check out sriov operator's code 92 | uses: actions/checkout@v4 93 | with: 94 | repository: k8snetworkplumbingwg/sriov-network-operator 95 | path: sriov-network-operator-wc 96 | ref: master 97 | 98 | - name: run test 99 | run: make test-e2e-conformance-virtual-k8s-cluster-ci 100 | working-directory: sriov-network-operator-wc 101 | env: 102 | LOCAL_SRIOV_CNI_IMAGE: ghaction-sriov-cni:pr-${{github.event.pull_request.number}} 103 | 104 | - uses: actions/upload-artifact@v4 105 | if: always() 106 | with: 107 | name: ${{ env.TEST_REPORT_PATH }} 108 | path: ./sriov-network-operator-wc/${{ env.TEST_REPORT_PATH }} 109 | -------------------------------------------------------------------------------- /pkg/logging/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | // package logging is a small wrapper around github.com/k8snetworkplumbingwg/cni-log 18 | 19 | package logging 20 | 21 | import ( 22 | cnilog "github.com/k8snetworkplumbingwg/cni-log" 23 | ) 24 | 25 | const ( 26 | labelCNIName = "cniName" 27 | labelContainerID = "containerID" 28 | labelNetNS = "netns" 29 | labelIFName = "ifname" 30 | cniName = "sriov-cni" 31 | ) 32 | 33 | var ( 34 | logLevelDefault = cnilog.InfoLevel 35 | containerID = "" 36 | netNS = "" 37 | ifName = "" 38 | ) 39 | 40 | // Init initializes logging with the requested parameters in this order: log level, log file, container ID, 41 | // network namespace and interface name. 42 | func Init(logLevel, logFile, containerIdentification, networkNamespace, interfaceName string) { 43 | setLogLevel(logLevel) 44 | setLogFile(logFile) 45 | containerID = containerIdentification 46 | netNS = networkNamespace 47 | ifName = interfaceName 48 | } 49 | 50 | // setLogLevel sets the log level to either verbose, debug, info, warn, error or panic. If an invalid string is 51 | // provided, it uses error. 52 | func setLogLevel(l string) { 53 | ll := cnilog.StringToLevel(l) 54 | if ll == cnilog.InvalidLevel { 55 | ll = logLevelDefault 56 | } 57 | cnilog.SetLogLevel(ll) 58 | } 59 | 60 | // setLogFile sets the log file for logging. If the empty string is provided, it uses stderr. 61 | func setLogFile(fileName string) { 62 | if fileName == "" { 63 | cnilog.SetLogStderr(true) 64 | cnilog.SetLogFile("") 65 | return 66 | } 67 | cnilog.SetLogFile(fileName) 68 | cnilog.SetLogStderr(false) 69 | } 70 | 71 | // Debug provides structured logging for log level >= debug. 72 | func Debug(msg string, args ...interface{}) { 73 | cnilog.DebugStructured(msg, prependArgs(args)...) 74 | } 75 | 76 | // Info provides structured logging for log level >= info. 77 | func Info(msg string, args ...interface{}) { 78 | cnilog.InfoStructured(msg, prependArgs(args)...) 79 | } 80 | 81 | // Warning provides structured logging for log level >= warning. 82 | func Warning(msg string, args ...interface{}) { 83 | cnilog.WarningStructured(msg, prependArgs(args)...) 84 | } 85 | 86 | // Error provides structured logging for log level >= error. 87 | func Error(msg string, args ...interface{}) { 88 | _ = cnilog.ErrorStructured(msg, prependArgs(args)...) 89 | } 90 | 91 | // Panic provides structured logging for log level >= panic. 92 | func Panic(msg string, args ...interface{}) { 93 | cnilog.PanicStructured(msg, prependArgs(args)...) 94 | } 95 | 96 | // prependArgs prepends cniName, containerID, netNS and ifName to the args of every log message. 97 | func prependArgs(args []interface{}) []interface{} { 98 | if ifName != "" { 99 | args = append([]interface{}{labelIFName, ifName}, args...) 100 | } 101 | if netNS != "" { 102 | args = append([]interface{}{labelNetNS, netNS}, args...) 103 | } 104 | if containerID != "" { 105 | args = append([]interface{}{labelContainerID, containerID}, args...) 106 | } 107 | args = append([]interface{}{labelCNIName, cniName}, args...) 108 | return args 109 | } 110 | -------------------------------------------------------------------------------- /pkg/sriov/mocks/pci_utils_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.50.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // PciUtils is an autogenerated mock type for the pciUtils type 8 | type PciUtils struct { 9 | mock.Mock 10 | } 11 | 12 | // EnableArpAndNdiscNotify provides a mock function with given fields: ifName 13 | func (_m *PciUtils) EnableArpAndNdiscNotify(ifName string) error { 14 | ret := _m.Called(ifName) 15 | 16 | if len(ret) == 0 { 17 | panic("no return value specified for EnableArpAndNdiscNotify") 18 | } 19 | 20 | var r0 error 21 | if rf, ok := ret.Get(0).(func(string) error); ok { 22 | r0 = rf(ifName) 23 | } else { 24 | r0 = ret.Error(0) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // EnableOptimisticDad provides a mock function with given fields: ifName 31 | func (_m *PciUtils) EnableOptimisticDad(ifName string) error { 32 | ret := _m.Called(ifName) 33 | 34 | if len(ret) == 0 { 35 | panic("no return value specified for EnableOptimisticDad") 36 | } 37 | 38 | var r0 error 39 | if rf, ok := ret.Get(0).(func(string) error); ok { 40 | r0 = rf(ifName) 41 | } else { 42 | r0 = ret.Error(0) 43 | } 44 | 45 | return r0 46 | } 47 | 48 | // GetPciAddress provides a mock function with given fields: ifName, vf 49 | func (_m *PciUtils) GetPciAddress(ifName string, vf int) (string, error) { 50 | ret := _m.Called(ifName, vf) 51 | 52 | if len(ret) == 0 { 53 | panic("no return value specified for GetPciAddress") 54 | } 55 | 56 | var r0 string 57 | var r1 error 58 | if rf, ok := ret.Get(0).(func(string, int) (string, error)); ok { 59 | return rf(ifName, vf) 60 | } 61 | if rf, ok := ret.Get(0).(func(string, int) string); ok { 62 | r0 = rf(ifName, vf) 63 | } else { 64 | r0 = ret.Get(0).(string) 65 | } 66 | 67 | if rf, ok := ret.Get(1).(func(string, int) error); ok { 68 | r1 = rf(ifName, vf) 69 | } else { 70 | r1 = ret.Error(1) 71 | } 72 | 73 | return r0, r1 74 | } 75 | 76 | // GetSriovNumVfs provides a mock function with given fields: ifName 77 | func (_m *PciUtils) GetSriovNumVfs(ifName string) (int, error) { 78 | ret := _m.Called(ifName) 79 | 80 | if len(ret) == 0 { 81 | panic("no return value specified for GetSriovNumVfs") 82 | } 83 | 84 | var r0 int 85 | var r1 error 86 | if rf, ok := ret.Get(0).(func(string) (int, error)); ok { 87 | return rf(ifName) 88 | } 89 | if rf, ok := ret.Get(0).(func(string) int); ok { 90 | r0 = rf(ifName) 91 | } else { 92 | r0 = ret.Get(0).(int) 93 | } 94 | 95 | if rf, ok := ret.Get(1).(func(string) error); ok { 96 | r1 = rf(ifName) 97 | } else { 98 | r1 = ret.Error(1) 99 | } 100 | 101 | return r0, r1 102 | } 103 | 104 | // GetVFLinkNamesFromVFID provides a mock function with given fields: pfName, vfID 105 | func (_m *PciUtils) GetVFLinkNamesFromVFID(pfName string, vfID int) ([]string, error) { 106 | ret := _m.Called(pfName, vfID) 107 | 108 | if len(ret) == 0 { 109 | panic("no return value specified for GetVFLinkNamesFromVFID") 110 | } 111 | 112 | var r0 []string 113 | var r1 error 114 | if rf, ok := ret.Get(0).(func(string, int) ([]string, error)); ok { 115 | return rf(pfName, vfID) 116 | } 117 | if rf, ok := ret.Get(0).(func(string, int) []string); ok { 118 | r0 = rf(pfName, vfID) 119 | } else { 120 | if ret.Get(0) != nil { 121 | r0 = ret.Get(0).([]string) 122 | } 123 | } 124 | 125 | if rf, ok := ret.Get(1).(func(string, int) error); ok { 126 | r1 = rf(pfName, vfID) 127 | } else { 128 | r1 = ret.Error(1) 129 | } 130 | 131 | return r0, r1 132 | } 133 | 134 | // NewPciUtils creates a new instance of PciUtils. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 135 | // The first argument is typically a *testing.T value. 136 | func NewPciUtils(t interface { 137 | mock.TestingT 138 | Cleanup(func()) 139 | }) *PciUtils { 140 | mock := &PciUtils{} 141 | mock.Mock.Test(t) 142 | 143 | t.Cleanup(func() { mock.AssertExpectations(t) }) 144 | 145 | return mock 146 | } 147 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package types 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | 23 | "github.com/containernetworking/cni/pkg/types" 24 | "github.com/vishvananda/netlink" 25 | ) 26 | 27 | const ( 28 | Proto8021q = "802.1q" 29 | Proto8021ad = "802.1ad" 30 | ) 31 | 32 | var VlanProtoInt = map[string]int{Proto8021q: 33024, Proto8021ad: 34984} 33 | 34 | // VfState represents the state of the VF 35 | type VfState struct { 36 | HostIFName string 37 | SpoofChk bool 38 | Trust bool 39 | AdminMAC string 40 | EffectiveMAC string 41 | Vlan int 42 | VlanQoS int 43 | VlanProto int 44 | MinTxRate int 45 | MaxTxRate int 46 | LinkState uint32 47 | MTU int 48 | } 49 | 50 | // FillFromVfInfo - Fill attributes according to the provided netlink.VfInfo struct 51 | func (vs *VfState) FillFromVfInfo(info *netlink.VfInfo) { 52 | vs.AdminMAC = info.Mac.String() 53 | vs.LinkState = info.LinkState 54 | vs.MaxTxRate = int(info.MaxTxRate) 55 | vs.MinTxRate = int(info.MinTxRate) 56 | vs.Vlan = info.Vlan 57 | vs.VlanQoS = info.Qos 58 | vs.VlanProto = info.VlanProto 59 | vs.SpoofChk = info.Spoofchk 60 | vs.Trust = info.Trust != 0 61 | } 62 | 63 | type NetConf struct { 64 | types.NetConf 65 | SriovNetConf 66 | } 67 | 68 | // NetConf extends types.NetConf for sriov-cni 69 | type SriovNetConf struct { 70 | OrigVfState VfState // Stores the original VF state as it was prior to any operations done during cmdAdd flow 71 | DPDKMode bool `json:"-"` 72 | Master string 73 | MAC string 74 | MTU *int // interface MTU 75 | Vlan *int `json:"vlan"` 76 | VlanQoS *int `json:"vlanQoS"` 77 | VlanProto *string `json:"vlanProto"` // 802.1ad|802.1q 78 | DeviceID string `json:"deviceID"` // PCI address of a VF in valid sysfs format 79 | VFID int 80 | MinTxRate *int `json:"min_tx_rate"` // Mbps, 0 = disable rate limiting 81 | MaxTxRate *int `json:"max_tx_rate"` // Mbps, 0 = disable rate limiting 82 | SpoofChk string `json:"spoofchk,omitempty"` // on|off 83 | Trust string `json:"trust,omitempty"` // on|off 84 | LinkState string `json:"link_state,omitempty"` // auto|enable|disable 85 | RuntimeConfig struct { 86 | Mac string `json:"mac,omitempty"` 87 | } `json:"runtimeConfig,omitempty"` 88 | LogLevel string `json:"logLevel,omitempty"` 89 | LogFile string `json:"logFile,omitempty"` 90 | } 91 | 92 | func (n *NetConf) MarshalJSON() ([]byte, error) { 93 | netConfBytes, err := json.Marshal(&n.NetConf) 94 | if err != nil { 95 | return nil, fmt.Errorf("error serializing delegate netConf: %v", err) 96 | } 97 | 98 | sriovNetConfBytes, err := json.Marshal(&n.SriovNetConf) 99 | if err != nil { 100 | return nil, fmt.Errorf("error serializing delegate sriovNetConf: %v", err) 101 | } 102 | 103 | netConfMap := make(map[string]interface{}) 104 | if err := json.Unmarshal(netConfBytes, &netConfMap); err != nil { 105 | return nil, err 106 | } 107 | 108 | sriovNetConfMap := make(map[string]interface{}) 109 | if err := json.Unmarshal(sriovNetConfBytes, &sriovNetConfMap); err != nil { 110 | return nil, err 111 | } 112 | 113 | for k, v := range netConfMap { 114 | sriovNetConfMap[k] = v 115 | } 116 | 117 | sriovNetConfBytes, err = json.Marshal(sriovNetConfMap) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return sriovNetConfBytes, nil 123 | } 124 | -------------------------------------------------------------------------------- /test/integration/test_concurrent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2025 sriov-cni authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | 19 | # shellcheck source-path=test/integration 20 | . libtest.sh 21 | 22 | test_concurrent_add_calls() { 23 | export CNI_IFNAME=net1 24 | 25 | create_network_ns "container_1" 26 | read -r -d '' CNI_INPUT <<- EOM 27 | { 28 | "type": "sriov", 29 | "cniVersion": "0.3.1", 30 | "name": "sriov-network", 31 | "ipam": { "type": "test-ipam-cni" }, 32 | "deviceID": "0000:af:06.0", 33 | "logFile": "${DEFAULT_CNI_DIR}/sriov.log", 34 | "logLevel": "debug" 35 | } 36 | EOM 37 | 38 | # Simulate a long CNI Add operation 39 | IPAM_MOCK_SLEEP=3 CNI_COMMAND=ADD invoke_sriov_cni > /dev/null & 40 | wait_for_file_to_exist "${DEFAULT_CNI_DIR}/pci/vf_lock/0000:af:06.0.lock" 41 | 42 | # Call CNI Add on the same PCI address 43 | create_network_ns "container_2" 44 | read -r -d '' CNI_INPUT <<- EOM 45 | { 46 | "type": "sriov", 47 | "cniVersion": "0.3.1", 48 | "name": "sriov-network", 49 | "ipam": { "type": "test-ipam-cni" }, 50 | "deviceID": "0000:af:06.0", 51 | "logFile": "${DEFAULT_CNI_DIR}/sriov.log", 52 | "logLevel": "debug" 53 | } 54 | EOM 55 | 56 | export CNI_COMMAND=ADD 57 | output=$(invoke_sriov_cni 2>/dev/null) 58 | assert_matches ".*pci address 0000:af:06.0 is already allocated.*" "$output" 59 | wait 60 | 61 | } 62 | 63 | 64 | # This test simulates a heavy load on the IPAM plugin, which takes a long time to finish. In this case, 65 | # the Kubelet can decide to remove the container with its network namespace while a CNI DEL command is still running. 66 | test_long_running_ipam() { 67 | 68 | create_network_ns "container_1" 69 | export CNI_CONTAINERID=container_1 70 | export CNI_IFNAME=net1 71 | 72 | read -r -d '' CNI_INPUT <<- EOM 73 | { 74 | "type": "sriov", 75 | "cniVersion": "0.3.1", 76 | "name": "sriov-network", 77 | "ipam": { 78 | "type": "test-ipam-cni" 79 | }, 80 | "deviceID": "0000:af:06.0", 81 | "vlan": 0, 82 | "logLevel": "debug", 83 | "logFile": "${DEFAULT_CNI_DIR}/sriov.log" 84 | } 85 | EOM 86 | 87 | export CNI_COMMAND=ADD 88 | assert invoke_sriov_cni 89 | 90 | # Start a long live CNI delete 91 | IPAM_MOCK_SLEEP=3 CNI_COMMAND=DEL invoke_sriov_cni & 92 | 93 | # Simulate the kubelet deleting the container and the network namespace after a timeout 94 | # The VF goes back to the root network namespace 95 | sleep 1 96 | ip netns exec container_1 ip link set net1 netns test_root_ns name enp175s6 97 | delete_network_ns container_1 98 | 99 | # Spawn a new container that tries to use the same device 100 | create_network_ns "container_2" 101 | export CNI_IFNAME=net1 102 | 103 | read -r -d '' CNI_INPUT <<- EOM 104 | { 105 | "type": "sriov", 106 | "cniVersion": "0.3.1", 107 | "name": "sriov-network", 108 | "vlan": 1234, 109 | "ipam": { 110 | "type": "test-ipam-cni" 111 | }, 112 | "deviceID": "0000:af:06.0", 113 | "logLevel": "debug", 114 | "logFile": "${DEFAULT_CNI_DIR}/sriov.log" 115 | } 116 | EOM 117 | 118 | export CNI_COMMAND=ADD 119 | assert invoke_sriov_cni 120 | assert_file_contains "${DEFAULT_CNI_DIR}/enp175s0f1.calls" "LinkSetVfVlanQosProto enp175s0f1 0 1234 0 33024" 121 | 122 | wait 123 | 124 | expected_vlan_set_calls=$(cat < We encourage contributor to test SRIOV CNI with various NICs to check the compatibility. 90 | > `cnitool` is a great way to test changes to sriov CNI in isolation. 91 | > More information on cnitool can be found [here](https://github.com/containernetworking/cni/tree/master/cnitool) 92 | 93 | ## Tools 94 | The project uses the Slack chat: 95 | - Slack: #[Intel-SDSG-slack](https://intel-corp.herokuapp.com/) channel on slack 96 | - Please contact contributors for issues and PR reviews. 97 | -------------------------------------------------------------------------------- /pkg/utils/netlink_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "net" 21 | 22 | "github.com/vishvananda/netlink" 23 | ) 24 | 25 | // Mocked netlink interface, this is required for unit tests 26 | 27 | // NetlinkManager is an interface to mock nelink library 28 | type NetlinkManager interface { 29 | LinkByName(string) (netlink.Link, error) 30 | LinkSetVfVlanQosProto(netlink.Link, int, int, int, int) error 31 | LinkSetVfHardwareAddr(netlink.Link, int, net.HardwareAddr) error 32 | LinkSetHardwareAddr(netlink.Link, net.HardwareAddr) error 33 | LinkSetUp(netlink.Link) error 34 | LinkSetDown(netlink.Link) error 35 | LinkSetNsFd(netlink.Link, int) error 36 | LinkSetName(netlink.Link, string) error 37 | LinkSetVfRate(netlink.Link, int, int, int) error 38 | LinkSetVfSpoofchk(netlink.Link, int, bool) error 39 | LinkSetVfTrust(netlink.Link, int, bool) error 40 | LinkSetVfState(netlink.Link, int, uint32) error 41 | LinkSetMTU(netlink.Link, int) error 42 | LinkDelAltName(netlink.Link, string) error 43 | } 44 | 45 | // MyNetlink NetlinkManager 46 | type MyNetlink struct { 47 | NetlinkManager 48 | } 49 | 50 | var netLinkLib NetlinkManager = &MyNetlink{} 51 | 52 | func GetNetlinkManager() NetlinkManager { 53 | return netLinkLib 54 | } 55 | 56 | // LinkByName implements NetlinkManager 57 | func (n *MyNetlink) LinkByName(name string) (netlink.Link, error) { 58 | return netlink.LinkByName(name) 59 | } 60 | 61 | // LinkSetVfVlanQosProto sets VLAN ID, QoS and Proto field for given VF using NetlinkManager 62 | func (n *MyNetlink) LinkSetVfVlanQosProto(link netlink.Link, vf, vlan, qos, proto int) error { 63 | return netlink.LinkSetVfVlanQosProto(link, vf, vlan, qos, proto) 64 | } 65 | 66 | // LinkSetVfHardwareAddr using NetlinkManager 67 | func (n *MyNetlink) LinkSetVfHardwareAddr(link netlink.Link, vf int, hwaddr net.HardwareAddr) error { 68 | return netlink.LinkSetVfHardwareAddr(link, vf, hwaddr) 69 | } 70 | 71 | // LinkSetHardwareAddr using NetlinkManager 72 | func (n *MyNetlink) LinkSetHardwareAddr(link netlink.Link, hwaddr net.HardwareAddr) error { 73 | return netlink.LinkSetHardwareAddr(link, hwaddr) 74 | } 75 | 76 | // LinkSetUp using NetlinkManager 77 | func (n *MyNetlink) LinkSetUp(link netlink.Link) error { 78 | return netlink.LinkSetUp(link) 79 | } 80 | 81 | // LinkSetDown using NetlinkManager 82 | func (n *MyNetlink) LinkSetDown(link netlink.Link) error { 83 | return netlink.LinkSetDown(link) 84 | } 85 | 86 | // LinkSetNsFd using NetlinkManager 87 | func (n *MyNetlink) LinkSetNsFd(link netlink.Link, fd int) error { 88 | return netlink.LinkSetNsFd(link, fd) 89 | } 90 | 91 | // LinkSetName using NetlinkManager 92 | func (n *MyNetlink) LinkSetName(link netlink.Link, name string) error { 93 | return netlink.LinkSetName(link, name) 94 | } 95 | 96 | // LinkSetVfRate using NetlinkManager 97 | func (n *MyNetlink) LinkSetVfRate(link netlink.Link, vf, minRate, maxRate int) error { 98 | return netlink.LinkSetVfRate(link, vf, minRate, maxRate) 99 | } 100 | 101 | // LinkSetVfSpoofchk using NetlinkManager 102 | func (n *MyNetlink) LinkSetVfSpoofchk(link netlink.Link, vf int, check bool) error { 103 | return netlink.LinkSetVfSpoofchk(link, vf, check) 104 | } 105 | 106 | // LinkSetVfTrust using NetlinkManager 107 | func (n *MyNetlink) LinkSetVfTrust(link netlink.Link, vf int, state bool) error { 108 | return netlink.LinkSetVfTrust(link, vf, state) 109 | } 110 | 111 | // LinkSetVfState using NetlinkManager 112 | func (n *MyNetlink) LinkSetVfState(link netlink.Link, vf int, state uint32) error { 113 | return netlink.LinkSetVfState(link, vf, state) 114 | } 115 | 116 | // LinkSetMTU using NetlinkManager 117 | func (n *MyNetlink) LinkSetMTU(link netlink.Link, mtu int) error { 118 | return netlink.LinkSetMTU(link, mtu) 119 | } 120 | 121 | // LinkDelAltName using NetlinkManager 122 | func (n *MyNetlink) LinkDelAltName(link netlink.Link, altName string) error { 123 | return netlink.LinkDelAltName(link, altName) 124 | } 125 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include make/license.mk 2 | 3 | # 4 | # Credit: 5 | # This makefile was adapted from: https://github.com/vincentbernat/hellogopher/blob/feature/glide/Makefile 6 | # 7 | # Package related 8 | BINARY_NAME=sriov 9 | PACKAGE=sriov-cni 10 | BINDIR=$(CURDIR)/bin 11 | BUILDDIR=$(CURDIR)/build 12 | PKGS = $(or $(PKG),$(shell go list ./... | grep -v ".*/mocks")) 13 | IMAGE_BUILDER ?= docker 14 | 15 | # Test settings 16 | TIMEOUT = 30 17 | COVERAGE_DIR = $(CURDIR)/test/coverage 18 | COVERAGE_MODE = atomic 19 | COVERAGE_PROFILE = $(COVERAGE_DIR)/cover-unit.out 20 | 21 | # Docker 22 | IMAGEDIR=$(CURDIR)/images 23 | DOCKERFILE?=$(CURDIR)/Dockerfile 24 | TAG?=ghcr.io/k8snetworkplumbingwg/sriov-cni 25 | # Accept proxy settings for docker 26 | DOCKERARGS= 27 | ifdef HTTP_PROXY 28 | DOCKERARGS += --build-arg http_proxy=$(HTTP_PROXY) 29 | endif 30 | ifdef HTTPS_PROXY 31 | DOCKERARGS += --build-arg https_proxy=$(HTTPS_PROXY) 32 | endif 33 | 34 | # Go settings 35 | GO = go 36 | GO_BUILD_OPTS ?=CGO_ENABLED=0 37 | GO_LDFLAGS ?= 38 | GO_FLAGS ?= 39 | GO_TAGS ?=-tags no_openssl 40 | export GOPATH?=$(shell go env GOPATH) 41 | 42 | # debug 43 | V ?= 0 44 | Q = $(if $(filter 1,$V),,@) 45 | 46 | .PHONY: all 47 | all: fmt lint build 48 | 49 | $(BINDIR) $(BUILDDIR) $(COVERAGE_DIR): ; $(info Creating directory $@...) 50 | @mkdir -p $@ 51 | 52 | .PHONY: build 53 | build: | $(BUILDDIR) ; $(info Building $(BINARY_NAME)...) @ ## Build SR-IOV CNI plugin 54 | $Q cd $(CURDIR)/cmd/$(BINARY_NAME) && $(GO_BUILD_OPTS) go build -ldflags '$(GO_LDFLAGS)' $(GO_FLAGS) -o $(BUILDDIR)/$(BINARY_NAME) $(GO_TAGS) -v 55 | $(info Done!) 56 | 57 | # Tools 58 | GOLANGCI_LINT = $(BINDIR)/golangci-lint 59 | GOLANGCI_LINT_VERSION = v1.64.7 60 | $(GOLANGCI_LINT): | $(BINDIR) ; $(info Installing golangci-lint...) 61 | $Q GOBIN=$(BINDIR) $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) 62 | 63 | # Tools 64 | MOCKERY = $(BINDIR)/mockery 65 | MOCKERY_VERSION = v2.50.2 66 | $(MOCKERY): | $(BINDIR) ; $(info Installing mockery...) 67 | $Q GOBIN=$(BINDIR) $(GO) install github.com/vektra/mockery/v2@$(MOCKERY_VERSION) 68 | 69 | # Tests 70 | TEST_TARGETS := test-default test-verbose test-race 71 | .PHONY: $(TEST_TARGETS) test 72 | test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting 73 | test-race: ARGS=-race ## Run tests with race detector 74 | $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) 75 | $(TEST_TARGETS): test 76 | test: ; $(info running $(NAME:%=% )tests...) @ ## Run tests 77 | $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(PKGS) 78 | 79 | .PHONY: test-coverage 80 | test-coverage: | $(COVERAGE_DIR) ; $(info Running coverage tests...) @ ## Run coverage tests 81 | $Q $(GO) test -timeout $(TIMEOUT)s -cover -covermode=$(COVERAGE_MODE) -coverprofile=$(COVERAGE_PROFILE) $(PKGS) 82 | 83 | .PHONY: lint 84 | lint: $(GOLANGCI_LINT) ; $(info Running golangci-lint linter...) @ ## Run golangci-lint linter 85 | $Q $(GOLANGCI_LINT) run 86 | 87 | .PHONY: mock-generate 88 | mock-generate: $(MOCKERY) ; $(info Running mockery...) @ ## Run golangci-lint linter 89 | $Q $(MOCKERY) --recursive=true --name=NetlinkManager --output=./pkg/utils/mocks/ --filename=netlink_manager_mock.go --exported --dir pkg/utils 90 | $Q $(MOCKERY) --recursive=true --name=pciUtils --output=./pkg/sriov/mocks/ --filename=pci_utils_mock.go --exported --dir pkg/sriov 91 | 92 | 93 | .PHONY: fmt 94 | fmt: ; $(info Running go fmt...) @ ## Run go fmt on all source files 95 | @ $(GO) fmt ./... 96 | 97 | .PHONY: vet 98 | vet: ; $(info Running go vet...) @ ## Run go vet on all source files 99 | @ $(GO) vet ./... 100 | 101 | # Docker image 102 | # To pass proxy for Docker invoke it as 'make image HTTP_POXY=http://192.168.0.1:8080' 103 | .PHONY: image 104 | image: ; $(info Building Docker image...) @ ## Build SR-IOV CNI docker image 105 | @$(IMAGE_BUILDER) build -t $(TAG) -f $(DOCKERFILE) $(CURDIR) $(DOCKERARGS) 106 | 107 | test-image: image 108 | $Q $(IMAGEDIR)/image_test.sh $(IMAGE_BUILDER) $(TAG) 109 | 110 | BASH_UNIT=$(BINDIR)/bash_unit 111 | $(BASH_UNIT): $(BINDIR) 112 | curl -L https://github.com/pgrange/bash_unit/raw/refs/tags/v2.3.2/bash_unit > bin/bash_unit 113 | chmod a+x bin/bash_unit 114 | 115 | test-integration: $(BASH_UNIT) 116 | mkdir -p $(COVERAGE_DIR)/integration 117 | GOCOVERDIR=$(COVERAGE_DIR)/integration $(BASH_UNIT) test/integration/test_*.sh 118 | go tool covdata textfmt -pkg github.com/k8snetworkplumbingwg/sriov-cni/... -i $(COVERAGE_DIR)/integration -o test/coverage/cover-integration.out 119 | 120 | GOCOVMERGE = $(BINDIR)/gocovmerge 121 | gocovmerge: ## Download gocovmerge locally if necessary. 122 | GOBIN=$(BINDIR) $(GO) install github.com/shabbyrobe/gocovmerge/cmd/gocovmerge@v0.0.0-20230507112040-c3350d9342df 123 | 124 | merge-test-coverage: gocovmerge 125 | $(GOCOVMERGE) $(COVERAGE_DIR)/cover-*.out > $(COVERAGE_DIR)/cover.out 126 | 127 | # Misc 128 | .PHONY: deps-update 129 | deps-update: ; $(info Updating dependencies...) @ ## Update dependencies 130 | @ $(GO) mod tidy 131 | 132 | .PHONY: clean 133 | clean: ; $(info Cleaning...) @ ## Cleanup everything 134 | @ $(GO) clean --modcache --cache --testcache 135 | @ rm -rf $(BUILDDIR) 136 | @ rm -rf $(BINDIR) 137 | @ rm -rf test/ 138 | 139 | .PHONY: help 140 | help: ; @ ## Display this help message 141 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 142 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 143 | -------------------------------------------------------------------------------- /pkg/utils/pci_allocator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path" 23 | "path/filepath" 24 | "time" 25 | 26 | "github.com/containernetworking/plugins/pkg/ns" 27 | "golang.org/x/sys/unix" 28 | 29 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/logging" 30 | ) 31 | 32 | const pciLockAcquireTimeout = 60 * time.Second 33 | 34 | type PCIAllocation interface { 35 | SaveAllocatedPCI(string, string) error 36 | DeleteAllocatedPCI(string) error 37 | IsAllocated(string) error 38 | } 39 | 40 | type PCIAllocator struct { 41 | dataDir string 42 | } 43 | 44 | // NewPCIAllocator returns a new PCI allocator 45 | // it will use the /pci folder to store the information about allocated PCI addresses 46 | func NewPCIAllocator(dataDir string) *PCIAllocator { 47 | return &PCIAllocator{dataDir: filepath.Join(dataDir, "pci")} 48 | } 49 | 50 | // Lock gets an exclusive lock on the given PCI address, ensuring there is no other process configuring / or de-configuring the same device. 51 | func (p *PCIAllocator) Lock(pciAddress string) error { 52 | lockDir := path.Join(p.dataDir, "vf_lock") 53 | if err := os.MkdirAll(lockDir, 0600); err != nil { 54 | return fmt.Errorf("failed to create the sriov lock directory(%q): %v", lockDir, err) 55 | } 56 | 57 | path := filepath.Join(lockDir, fmt.Sprintf("%s.lock", pciAddress)) 58 | 59 | // unix.O_CREAT - Create the file if it doesn't exist 60 | // unix.O_RDONLY - Open the file for read 61 | // unix.O_CLOEXEC - Automatically close the file on exit. This is useful to keep the flock until the process exits 62 | fd, err := unix.Open(path, unix.O_CREAT|unix.O_RDONLY|unix.O_CLOEXEC, 0600) 63 | if err != nil { 64 | return fmt.Errorf("failed to open PCI file [%s] for locking: %w", path, err) 65 | } 66 | 67 | errCh := make(chan error) 68 | go func() { 69 | // unix.LOCK_EX - Exclusive lock 70 | errCh <- unix.Flock(fd, unix.LOCK_EX) 71 | }() 72 | 73 | select { 74 | case err = <-errCh: 75 | if err != nil { 76 | return fmt.Errorf("failed to flock PCI file [%s]: %w", path, err) 77 | } 78 | return nil 79 | 80 | case <-time.After(pciLockAcquireTimeout): 81 | return fmt.Errorf("time out while waiting to acquire exclusive lock on [%s]", path) 82 | } 83 | } 84 | 85 | // SaveAllocatedPCI creates a file with the pci address as a name and the network namespace as the content 86 | // return error if the file was not created 87 | func (p *PCIAllocator) SaveAllocatedPCI(pciAddress, ns string) error { 88 | if err := os.MkdirAll(p.dataDir, 0600); err != nil { 89 | return fmt.Errorf("failed to create the sriov data directory(%q): %v", p.dataDir, err) 90 | } 91 | 92 | path := filepath.Join(p.dataDir, pciAddress) 93 | err := os.WriteFile(path, []byte(ns), 0600) 94 | if err != nil { 95 | return fmt.Errorf("failed to write used PCI address lock file in the path(%q): %v", path, err) 96 | } 97 | 98 | return err 99 | } 100 | 101 | // DeleteAllocatedPCI Remove the allocated PCI file 102 | // return error if the file doesn't exist 103 | func (p *PCIAllocator) DeleteAllocatedPCI(pciAddress string) error { 104 | path := filepath.Join(p.dataDir, pciAddress) 105 | if err := os.Remove(path); err != nil { 106 | return fmt.Errorf("error removing PCI address lock file %s: %v", path, err) 107 | } 108 | return nil 109 | } 110 | 111 | // IsAllocated checks if the PCI address file exist 112 | // if it exists we also check the network namespace still exist if not we delete the allocation 113 | // The function will return an error if the pci is still allocated to a running pod 114 | func (p *PCIAllocator) IsAllocated(pciAddress string) (bool, error) { 115 | path := filepath.Join(p.dataDir, pciAddress) 116 | _, err := os.Stat(path) 117 | if err != nil { 118 | if os.IsNotExist(err) { 119 | return false, nil 120 | } 121 | 122 | return false, fmt.Errorf("failed to check for pci address file for %s: %v", path, err) 123 | } 124 | 125 | dat, err := os.ReadFile(path) 126 | if err != nil { 127 | return false, fmt.Errorf("failed to read for pci address file for %s: %v", path, err) 128 | } 129 | 130 | // To prevent a locking of a PCI address for every pciAddress file we also add the netns path where it's been used 131 | // This way if for some reason the cmdDel command was not called but the pod namespace doesn't exist anymore 132 | // we release the PCI address 133 | networkNamespace, err := ns.GetNS(string(dat)) 134 | if err != nil { 135 | logging.Debug("Mark the PCI address as released", 136 | "func", "IsAllocated", 137 | "pciAddress", pciAddress) 138 | err = p.DeleteAllocatedPCI(pciAddress) 139 | if err != nil { 140 | return false, fmt.Errorf("error deleting the pci allocation for vf pci address %s: %v", pciAddress, err) 141 | } 142 | 143 | return false, nil 144 | } 145 | 146 | // Close the network namespace 147 | networkNamespace.Close() 148 | return true, nil 149 | } 150 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= 2 | github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 4 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 5 | github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= 6 | github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= 7 | github.com/containernetworking/plugins v1.8.1-0.20251002142623-372953dfb89f h1:+2jbLXdXfgb3Yn8xpmjAiwK7SskgT9zx376q3T6VF7c= 8 | github.com/containernetworking/plugins v1.8.1-0.20251002142623-372953dfb89f/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= 9 | github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= 10 | github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 14 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 15 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 16 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 17 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 18 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 19 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= 20 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= 21 | github.com/k8snetworkplumbingwg/cni-log v0.0.0-20230801160229-b6e062c9e0f2 h1:KB8UPZQwLge4Abuk9tNmvzffdCJgqXSN341BX98QTHg= 22 | github.com/k8snetworkplumbingwg/cni-log v0.0.0-20230801160229-b6e062c9e0f2/go.mod h1:/x45AlZDoJVSSV4ECDb5TcHLzrVRDllsCMDzMrtHKwk= 23 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 24 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 25 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 26 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 27 | github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= 28 | github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= 29 | github.com/onsi/ginkgo/v2 v2.25.2 h1:hepmgwx1D+llZleKQDMEvy8vIlCxMGt7W5ZxDjIEhsw= 30 | github.com/onsi/ginkgo/v2 v2.25.2/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= 31 | github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 32 | github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 33 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 34 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 38 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 39 | github.com/safchain/ethtool v0.6.2 h1:O3ZPFAKEUEfbtE6J/feEe2Ft7dIJ2Sy8t4SdMRiIMHY= 40 | github.com/safchain/ethtool v0.6.2/go.mod h1:VS7cn+bP3Px3rIq55xImBiZGHVLNyBh5dqG6dDQy8+I= 41 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 42 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 43 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 44 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 45 | github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= 46 | github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= 47 | github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= 48 | github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 49 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 50 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 51 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 52 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 53 | golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= 54 | golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 55 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 58 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 59 | golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= 60 | golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 61 | golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= 62 | golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 63 | google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= 64 | google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 67 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 69 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 70 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 71 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 72 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 73 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= 75 | sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/k8snetworkplumbingwg/sriov-cni.svg?branch=master)](https://travis-ci.org/k8snetworkplumbingwg/sriov-cni) [![Go Report Card](https://goreportcard.com/badge/github.com/k8snetworkplumbingwg/sriov-cni)](https://goreportcard.com/report/github.com/k8snetworkplumbingwg/sriov-cni) [![Weekly minutes](https://img.shields.io/badge/Weekly%20Meeting%20Minutes-Mon%203pm%20GMT-blue.svg?style=plastic)](https://docs.google.com/document/d/1sJQMHbxZdeYJPgAWK1aSt6yzZ4K_8es7woVIrwinVwI) [![Coverage Status](https://coveralls.io/repos/github/k8snetworkplumbingwg/sriov-cni/badge.svg?branch=master)](https://coveralls.io/github/k8snetworkplumbingwg/sriov-cni?branch=master) 2 | 3 | * [SR-IOV CNI plugin](#sr-iov-cni-plugin) 4 | * [Build](#build) 5 | * [Kubernetes Quick Start](#kubernetes-quick-start) 6 | * [Usage](#usage) 7 | * [Basic configuration parameters](#basic-configuration-parameters) 8 | * [Example configurations](#example-configurations) 9 | * [Kernel driver config](#kernel-driver-config) 10 | * [Advanced kernel driver config](#advanced-kernel-driver-config) 11 | * [DPDK userspace driver config](#dpdk-userspace-driver-config) 12 | * [Advanced configuration](#advanced-configuration) 13 | * [Contributing](#contributing) 14 | 15 | # SR-IOV CNI plugin 16 | This plugin enables the configuration and usage of SR-IOV VF networks in containers and orchestrators like Kubernetes. 17 | 18 | Network Interface Cards (NICs) with [SR-IOV](http://blog.scottlowe.org/2009/12/02/what-is-sr-iov/) capabilities are managed through physical functions (PFs) and virtual functions (VFs). A PF is used by the host and usually represents a single NIC port. VF configurations are applied through the PF. With SR-IOV CNI each VF can be treated as a separate network interface, assigned to a container, and configured with it's own MAC, VLAN, IP and more. 19 | 20 | SR-IOV CNI plugin works with [SR-IOV device plugin](https://github.com/k8snetworkplumbingwg/sriov-network-device-plugin) for VF allocation in Kubernetes. A metaplugin such as [Multus](https://github.com/intel/multus-cni) gets the allocated VF's `deviceID`(PCI address) and is responsible for invoking the SR-IOV CNI plugin with that `deviceID`. 21 | 22 | ## Build 23 | 24 | This plugin uses Go modules for dependency management and requires Go 1.17+ to build. 25 | 26 | To build the plugin binary: 27 | 28 | `` 29 | make 30 | `` 31 | 32 | Upon successful build the plugin binary will be available in `build/sriov`. 33 | 34 | ## Kubernetes Quick Start 35 | A full guide on orchestrating SR-IOV virtual functions in Kubernetes can be found at the [SR-IOV Device Plugin project.](https://github.com/k8snetworkplumbingwg/sriov-network-device-plugin#quick-start) 36 | 37 | Creating VFs is outside the scope of the SR-IOV CNI plugin. [More information about allocating VFs on different NICs can be found here](https://github.com/k8snetworkplumbingwg/sriov-network-device-plugin/blob/master/docs/vf-setup.md) 38 | 39 | To deploy SR-IOV CNI by itself on a Kubernetes 1.16+ cluster: 40 | 41 | `kubectl apply -f images/sriov-cni-daemonset.yaml` 42 | 43 | **Note** The above deployment is not sufficient to manage and configure SR-IOV virtual functions. [See the full orchestration guide for more information.](https://github.com/k8snetworkplumbingwg/sriov-network-device-plugin#sr-iov-network-device-plugin) 44 | 45 | 46 | ## Usage 47 | SR-IOV CNI networks are commonly configured using Multus and SR-IOV Device Plugin using Network Attachment Definitions. More information about configuring Kubernetes networks using this pattern can be found in the [Multus configuration reference document.](https://github.com/k8snetworkplumbingwg/multus-cni/blob/master/docs/configuration.md) 48 | 49 | A Network Attachment Definition for SR-IOV CNI takes the form: 50 | 51 | ``` 52 | apiVersion: "k8s.cni.cncf.io/v1" 53 | kind: NetworkAttachmentDefinition 54 | metadata: 55 | name: sriov-net1 56 | annotations: 57 | k8s.v1.cni.cncf.io/resourceName: intel.com/intel_sriov_netdevice 58 | spec: 59 | config: '{ 60 | "type": "sriov", 61 | "cniVersion": "0.3.1", 62 | "name": "sriov-network", 63 | "ipam": { 64 | "type": "host-local", 65 | "subnet": "10.56.217.0/24", 66 | "routes": [{ 67 | "dst": "0.0.0.0/0" 68 | }], 69 | "gateway": "10.56.217.1" 70 | } 71 | }' 72 | ``` 73 | 74 | The `.spec.config` field contains the configuration information used by the SR-IOV CNI. 75 | 76 | ### Basic configuration parameters 77 | 78 | The following parameters are generic parameters which are not specific to the SR-IOV CNI configuration, though (with the exception of ipam) they need to be included in the config. 79 | 80 | * `cniVersion` : the version of the CNI spec used. 81 | * `type` : CNI plugin used. "sriov" corresponds to SR-IOV CNI. 82 | * `name` : the name of the network created. 83 | * `ipam` (optional) : the configuration of the IP Address Management plugin. Required to designate an IP for a kernel interface. 84 | 85 | ### Example configurations 86 | The following examples show the config needed to set up basic SR-IOV networking in a container. Each of the json config objects below can be placed in the `.spec.config` field of a Network Attachment Definition to integrate with Multus. 87 | 88 | #### Kernel driver config 89 | This is the minimum configuration for a working kernel driver interface using an SR-IOV Virtual Function. It applies an IP address using the host-local IPAM plugin in the range of the subnet provided. 90 | 91 | ```json 92 | { 93 | "type": "sriov", 94 | "cniVersion": "0.3.1", 95 | "name": "sriov-network", 96 | "ipam": { 97 | "type": "host-local", 98 | "subnet": "10.56.217.0/24", 99 | "routes": [{ 100 | "dst": "0.0.0.0/0" 101 | }], 102 | "gateway": "10.56.217.1" 103 | } 104 | } 105 | ``` 106 | 107 | #### Extended kernel driver config 108 | This configuration sets a number of extra parameters that may be key for SR-IOV networks including a vlan tag, disabled spoof checking and enabled trust mode. These parameters are commonly set in more advanced SR-IOV VF based networks. 109 | 110 | ```json 111 | { 112 | "cniVersion": "0.3.1", 113 | "name": "sriov-advanced", 114 | "type": "sriov", 115 | "vlan": 1000, 116 | "spoofchk": "off", 117 | "trust": "on", 118 | "ipam": { 119 | "type": "host-local", 120 | "subnet": "10.56.217.0/24", 121 | "routes": [{ 122 | "dst": "0.0.0.0/0" 123 | }], 124 | "gateway": "10.56.217.1" 125 | } 126 | } 127 | ``` 128 | 129 | #### DPDK userspace driver config 130 | 131 | The below config will configure a VF using a userspace driver (uio/vfio) for use in a container. If this plugin is used with a VF bound to a dpdk driver then the IPAM configuration will still be respected, but it will only allocate IP address(es) using the specified IPAM plugin, not apply the IP address(es) to container interface. Other config parameters should be applicable but implementation may be driver specific. 132 | 133 | ```json 134 | { 135 | "cniVersion": "0.3.1", 136 | "name": "sriov-dpdk", 137 | "type": "sriov", 138 | "vlan": 1000 139 | } 140 | ``` 141 | 142 | **Note** [DHCP](https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp) IPAM plugin can not be used for VF bound to a dpdk driver (uio/vfio). 143 | 144 | **Note** When VLAN is not specified in the Network-Attachment-Definition, or when it is given a value of 0, 145 | VFs connected to this network will have no vlan tag. 146 | 147 | 148 | ### Advanced Configuration 149 | 150 | SR-IOV CNI allows the setting of other SR-IOV options such as link-state and quality of service parameters. To learn more about how these parameters are set consult the [SR-IOV CNI configuration reference guide](docs/configuration-reference.md) 151 | 152 | ## Contributing 153 | To report a bug or request a feature, open an issue on this repo using one of the available templates. 154 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package config 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "path/filepath" 23 | "strings" 24 | 25 | "github.com/containernetworking/cni/pkg/skel" 26 | 27 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/logging" 28 | sriovtypes "github.com/k8snetworkplumbingwg/sriov-cni/pkg/types" 29 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils" 30 | ) 31 | 32 | var ( 33 | // DefaultCNIDir used for caching NetConf 34 | DefaultCNIDir = "/var/lib/cni/sriov" 35 | ) 36 | 37 | // SetLogging sets global logging parameters. 38 | func SetLogging(stdinData []byte, containerID, netns, ifName string) error { 39 | n := &sriovtypes.NetConf{} 40 | if err := json.Unmarshal(stdinData, n); err != nil { 41 | return fmt.Errorf("SetLogging(): failed to load netconf: %v", err) 42 | } 43 | 44 | logging.Init(n.LogLevel, n.LogFile, containerID, netns, ifName) 45 | return nil 46 | } 47 | 48 | // LoadConf parses and validates stdin netconf and returns NetConf object 49 | func LoadConf(bytes []byte) (*sriovtypes.NetConf, error) { 50 | n := &sriovtypes.NetConf{} 51 | if err := json.Unmarshal(bytes, n); err != nil { 52 | return nil, fmt.Errorf("LoadConf(): failed to load netconf: %v", err) 53 | } 54 | 55 | // DeviceID takes precedence; if we are given a VF pciaddr then work from there 56 | if n.DeviceID != "" { 57 | // Get rest of the VF information 58 | pfName, vfID, err := getVfInfo(n.DeviceID) 59 | if err != nil { 60 | return nil, fmt.Errorf("LoadConf(): failed to get VF information: %q", err) 61 | } 62 | n.VFID = vfID 63 | n.Master = pfName 64 | } else { 65 | return nil, fmt.Errorf("LoadConf(): VF pci addr is required") 66 | } 67 | 68 | allocator := utils.NewPCIAllocator(DefaultCNIDir) 69 | err := allocator.Lock(n.DeviceID) 70 | if err != nil { 71 | return nil, err 72 | } 73 | logging.Debug("Acquired device lock", 74 | "func", "LoadConf", 75 | "DeviceID", n.DeviceID) 76 | 77 | // Check if the device is already allocated. 78 | // This is to prevent issues where kubelet request to delete a pod and in the same time a new pod using the same 79 | // vf is started. we can have an issue where the cmdDel of the old pod is called AFTER the cmdAdd of the new one 80 | // This will block the new pod creation until the cmdDel is done. 81 | logging.Debug("Check if the device is already allocated", 82 | "func", "LoadConf", 83 | "DefaultCNIDir", DefaultCNIDir, 84 | "n.DeviceID", n.DeviceID) 85 | isAllocated, err := allocator.IsAllocated(n.DeviceID) 86 | if err != nil { 87 | return n, err 88 | } 89 | 90 | if isAllocated { 91 | return n, fmt.Errorf("pci address %s is already allocated", n.DeviceID) 92 | } 93 | 94 | // Assuming VF is netdev interface; Get interface name(s) 95 | hostIFName, err := utils.GetVFLinkName(n.DeviceID) 96 | if err != nil || hostIFName == "" { 97 | // VF interface not found; check if VF has dpdk driver 98 | hasDpdkDriver, err := utils.HasDpdkDriver(n.DeviceID) 99 | if err != nil { 100 | return nil, fmt.Errorf("LoadConf(): failed to detect if VF %s has dpdk driver %q", n.DeviceID, err) 101 | } 102 | n.DPDKMode = hasDpdkDriver 103 | } 104 | 105 | if hostIFName != "" { 106 | n.OrigVfState.HostIFName = hostIFName 107 | } 108 | 109 | if hostIFName == "" && !n.DPDKMode { 110 | return nil, fmt.Errorf("LoadConf(): the VF %s does not have a interface name or a dpdk driver", n.DeviceID) 111 | } 112 | 113 | if n.Vlan == nil { 114 | // validate non-nil value for vlan qos 115 | if n.VlanQoS != nil { 116 | return nil, fmt.Errorf("LoadConf(): vlan id must be configured to set vlan QoS to a non-nil value") 117 | } 118 | 119 | // validate non-nil value for vlan proto 120 | if n.VlanProto != nil { 121 | return nil, fmt.Errorf("LoadConf(): vlan id must be configured to set vlan proto to a non-nil value") 122 | } 123 | } else { 124 | // validate vlan id range 125 | if *n.Vlan < 0 || *n.Vlan > 4094 { 126 | return nil, fmt.Errorf("LoadConf(): vlan id %d invalid: value must be in the range 0-4094", *n.Vlan) 127 | } 128 | 129 | if n.VlanQoS == nil { 130 | qos := 0 131 | n.VlanQoS = &qos 132 | } 133 | 134 | // validate that VLAN QoS is in the 0-7 range 135 | if *n.VlanQoS < 0 || *n.VlanQoS > 7 { 136 | return nil, fmt.Errorf("LoadConf(): vlan QoS PCP %d invalid: value must be in the range 0-7", *n.VlanQoS) 137 | } 138 | 139 | // validate non-zero value for vlan id if vlan qos is set to a non-zero value 140 | if *n.VlanQoS != 0 && *n.Vlan == 0 { 141 | return nil, fmt.Errorf("LoadConf(): non-zero vlan id must be configured to set vlan QoS to a non-zero value") 142 | } 143 | 144 | if n.VlanProto == nil { 145 | proto := sriovtypes.Proto8021q 146 | n.VlanProto = &proto 147 | } 148 | 149 | *n.VlanProto = strings.ToLower(*n.VlanProto) 150 | if *n.VlanProto != sriovtypes.Proto8021ad && *n.VlanProto != sriovtypes.Proto8021q { 151 | return nil, fmt.Errorf("LoadConf(): vlan Proto %s invalid: value must be '802.1Q' or '802.1ad'", *n.VlanProto) 152 | } 153 | 154 | // validate non-zero value for vlan id if vlan proto is set to 802.1ad 155 | if *n.VlanProto == sriovtypes.Proto8021ad && *n.Vlan == 0 { 156 | return nil, fmt.Errorf("LoadConf(): non-zero vlan id must be configured to set vlan proto 802.1ad") 157 | } 158 | } 159 | 160 | // validate that link state is one of supported values 161 | if n.LinkState != "" && n.LinkState != "auto" && n.LinkState != "enable" && n.LinkState != "disable" { 162 | return nil, fmt.Errorf("LoadConf(): invalid link_state value: %s", n.LinkState) 163 | } 164 | 165 | return n, nil 166 | } 167 | 168 | func getVfInfo(vfPci string) (string, int, error) { 169 | var vfID int 170 | 171 | pf, err := utils.GetPfName(vfPci) 172 | if err != nil { 173 | return "", vfID, err 174 | } 175 | 176 | vfID, err = utils.GetVfid(vfPci, pf) 177 | if err != nil { 178 | return "", vfID, err 179 | } 180 | 181 | return pf, vfID, nil 182 | } 183 | 184 | // LoadConfFromCache retrieves cached NetConf returns it along with a handle for removal 185 | func LoadConfFromCache(args *skel.CmdArgs) (*sriovtypes.NetConf, string, error) { 186 | netConf := &sriovtypes.NetConf{} 187 | 188 | s := []string{args.ContainerID, args.IfName} 189 | cRef := strings.Join(s, "-") 190 | cRefPath := filepath.Join(DefaultCNIDir, cRef) 191 | 192 | netConfBytes, err := utils.ReadScratchNetConf(cRefPath) 193 | if err != nil { 194 | return nil, "", fmt.Errorf("error reading cached NetConf in %s with name %s", DefaultCNIDir, cRef) 195 | } 196 | 197 | if err = json.Unmarshal(netConfBytes, netConf); err != nil { 198 | return nil, "", fmt.Errorf("failed to parse NetConf: %q", err) 199 | } 200 | 201 | return netConf, cRefPath, nil 202 | } 203 | 204 | // GetMacAddressForResult return the mac address we should report to the CNI call return object 205 | // if the device is on kernel mode we report that one back 206 | // if not we check the administrative mac address on the PF 207 | // if it is set and is not zero, report it. 208 | func GetMacAddressForResult(netConf *sriovtypes.NetConf) string { 209 | if netConf.MAC != "" { 210 | return netConf.MAC 211 | } 212 | if !netConf.DPDKMode { 213 | return netConf.OrigVfState.EffectiveMAC 214 | } 215 | if netConf.OrigVfState.AdminMAC != "00:00:00:00:00:00" { 216 | return netConf.OrigVfState.AdminMAC 217 | } 218 | 219 | return "" 220 | } 221 | -------------------------------------------------------------------------------- /pkg/utils/mocks/netlink_manager_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.50.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | net "net" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | 10 | netlink "github.com/vishvananda/netlink" 11 | ) 12 | 13 | // NetlinkManager is an autogenerated mock type for the NetlinkManager type 14 | type NetlinkManager struct { 15 | mock.Mock 16 | } 17 | 18 | // LinkByName provides a mock function with given fields: _a0 19 | func (_m *NetlinkManager) LinkByName(_a0 string) (netlink.Link, error) { 20 | ret := _m.Called(_a0) 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for LinkByName") 24 | } 25 | 26 | var r0 netlink.Link 27 | var r1 error 28 | if rf, ok := ret.Get(0).(func(string) (netlink.Link, error)); ok { 29 | return rf(_a0) 30 | } 31 | if rf, ok := ret.Get(0).(func(string) netlink.Link); ok { 32 | r0 = rf(_a0) 33 | } else { 34 | if ret.Get(0) != nil { 35 | r0 = ret.Get(0).(netlink.Link) 36 | } 37 | } 38 | 39 | if rf, ok := ret.Get(1).(func(string) error); ok { 40 | r1 = rf(_a0) 41 | } else { 42 | r1 = ret.Error(1) 43 | } 44 | 45 | return r0, r1 46 | } 47 | 48 | // LinkDelAltName provides a mock function with given fields: _a0, _a1 49 | func (_m *NetlinkManager) LinkDelAltName(_a0 netlink.Link, _a1 string) error { 50 | ret := _m.Called(_a0, _a1) 51 | 52 | if len(ret) == 0 { 53 | panic("no return value specified for LinkDelAltName") 54 | } 55 | 56 | var r0 error 57 | if rf, ok := ret.Get(0).(func(netlink.Link, string) error); ok { 58 | r0 = rf(_a0, _a1) 59 | } else { 60 | r0 = ret.Error(0) 61 | } 62 | 63 | return r0 64 | } 65 | 66 | // LinkSetDown provides a mock function with given fields: _a0 67 | func (_m *NetlinkManager) LinkSetDown(_a0 netlink.Link) error { 68 | ret := _m.Called(_a0) 69 | 70 | if len(ret) == 0 { 71 | panic("no return value specified for LinkSetDown") 72 | } 73 | 74 | var r0 error 75 | if rf, ok := ret.Get(0).(func(netlink.Link) error); ok { 76 | r0 = rf(_a0) 77 | } else { 78 | r0 = ret.Error(0) 79 | } 80 | 81 | return r0 82 | } 83 | 84 | // LinkSetHardwareAddr provides a mock function with given fields: _a0, _a1 85 | func (_m *NetlinkManager) LinkSetHardwareAddr(_a0 netlink.Link, _a1 net.HardwareAddr) error { 86 | ret := _m.Called(_a0, _a1) 87 | 88 | if len(ret) == 0 { 89 | panic("no return value specified for LinkSetHardwareAddr") 90 | } 91 | 92 | var r0 error 93 | if rf, ok := ret.Get(0).(func(netlink.Link, net.HardwareAddr) error); ok { 94 | r0 = rf(_a0, _a1) 95 | } else { 96 | r0 = ret.Error(0) 97 | } 98 | 99 | return r0 100 | } 101 | 102 | // LinkSetMTU provides a mock function with given fields: _a0, _a1 103 | func (_m *NetlinkManager) LinkSetMTU(_a0 netlink.Link, _a1 int) error { 104 | ret := _m.Called(_a0, _a1) 105 | 106 | if len(ret) == 0 { 107 | panic("no return value specified for LinkSetMTU") 108 | } 109 | 110 | var r0 error 111 | if rf, ok := ret.Get(0).(func(netlink.Link, int) error); ok { 112 | r0 = rf(_a0, _a1) 113 | } else { 114 | r0 = ret.Error(0) 115 | } 116 | 117 | return r0 118 | } 119 | 120 | // LinkSetName provides a mock function with given fields: _a0, _a1 121 | func (_m *NetlinkManager) LinkSetName(_a0 netlink.Link, _a1 string) error { 122 | ret := _m.Called(_a0, _a1) 123 | 124 | if len(ret) == 0 { 125 | panic("no return value specified for LinkSetName") 126 | } 127 | 128 | var r0 error 129 | if rf, ok := ret.Get(0).(func(netlink.Link, string) error); ok { 130 | r0 = rf(_a0, _a1) 131 | } else { 132 | r0 = ret.Error(0) 133 | } 134 | 135 | return r0 136 | } 137 | 138 | // LinkSetNsFd provides a mock function with given fields: _a0, _a1 139 | func (_m *NetlinkManager) LinkSetNsFd(_a0 netlink.Link, _a1 int) error { 140 | ret := _m.Called(_a0, _a1) 141 | 142 | if len(ret) == 0 { 143 | panic("no return value specified for LinkSetNsFd") 144 | } 145 | 146 | var r0 error 147 | if rf, ok := ret.Get(0).(func(netlink.Link, int) error); ok { 148 | r0 = rf(_a0, _a1) 149 | } else { 150 | r0 = ret.Error(0) 151 | } 152 | 153 | return r0 154 | } 155 | 156 | // LinkSetUp provides a mock function with given fields: _a0 157 | func (_m *NetlinkManager) LinkSetUp(_a0 netlink.Link) error { 158 | ret := _m.Called(_a0) 159 | 160 | if len(ret) == 0 { 161 | panic("no return value specified for LinkSetUp") 162 | } 163 | 164 | var r0 error 165 | if rf, ok := ret.Get(0).(func(netlink.Link) error); ok { 166 | r0 = rf(_a0) 167 | } else { 168 | r0 = ret.Error(0) 169 | } 170 | 171 | return r0 172 | } 173 | 174 | // LinkSetVfHardwareAddr provides a mock function with given fields: _a0, _a1, _a2 175 | func (_m *NetlinkManager) LinkSetVfHardwareAddr(_a0 netlink.Link, _a1 int, _a2 net.HardwareAddr) error { 176 | ret := _m.Called(_a0, _a1, _a2) 177 | 178 | if len(ret) == 0 { 179 | panic("no return value specified for LinkSetVfHardwareAddr") 180 | } 181 | 182 | var r0 error 183 | if rf, ok := ret.Get(0).(func(netlink.Link, int, net.HardwareAddr) error); ok { 184 | r0 = rf(_a0, _a1, _a2) 185 | } else { 186 | r0 = ret.Error(0) 187 | } 188 | 189 | return r0 190 | } 191 | 192 | // LinkSetVfRate provides a mock function with given fields: _a0, _a1, _a2, _a3 193 | func (_m *NetlinkManager) LinkSetVfRate(_a0 netlink.Link, _a1 int, _a2 int, _a3 int) error { 194 | ret := _m.Called(_a0, _a1, _a2, _a3) 195 | 196 | if len(ret) == 0 { 197 | panic("no return value specified for LinkSetVfRate") 198 | } 199 | 200 | var r0 error 201 | if rf, ok := ret.Get(0).(func(netlink.Link, int, int, int) error); ok { 202 | r0 = rf(_a0, _a1, _a2, _a3) 203 | } else { 204 | r0 = ret.Error(0) 205 | } 206 | 207 | return r0 208 | } 209 | 210 | // LinkSetVfSpoofchk provides a mock function with given fields: _a0, _a1, _a2 211 | func (_m *NetlinkManager) LinkSetVfSpoofchk(_a0 netlink.Link, _a1 int, _a2 bool) error { 212 | ret := _m.Called(_a0, _a1, _a2) 213 | 214 | if len(ret) == 0 { 215 | panic("no return value specified for LinkSetVfSpoofchk") 216 | } 217 | 218 | var r0 error 219 | if rf, ok := ret.Get(0).(func(netlink.Link, int, bool) error); ok { 220 | r0 = rf(_a0, _a1, _a2) 221 | } else { 222 | r0 = ret.Error(0) 223 | } 224 | 225 | return r0 226 | } 227 | 228 | // LinkSetVfState provides a mock function with given fields: _a0, _a1, _a2 229 | func (_m *NetlinkManager) LinkSetVfState(_a0 netlink.Link, _a1 int, _a2 uint32) error { 230 | ret := _m.Called(_a0, _a1, _a2) 231 | 232 | if len(ret) == 0 { 233 | panic("no return value specified for LinkSetVfState") 234 | } 235 | 236 | var r0 error 237 | if rf, ok := ret.Get(0).(func(netlink.Link, int, uint32) error); ok { 238 | r0 = rf(_a0, _a1, _a2) 239 | } else { 240 | r0 = ret.Error(0) 241 | } 242 | 243 | return r0 244 | } 245 | 246 | // LinkSetVfTrust provides a mock function with given fields: _a0, _a1, _a2 247 | func (_m *NetlinkManager) LinkSetVfTrust(_a0 netlink.Link, _a1 int, _a2 bool) error { 248 | ret := _m.Called(_a0, _a1, _a2) 249 | 250 | if len(ret) == 0 { 251 | panic("no return value specified for LinkSetVfTrust") 252 | } 253 | 254 | var r0 error 255 | if rf, ok := ret.Get(0).(func(netlink.Link, int, bool) error); ok { 256 | r0 = rf(_a0, _a1, _a2) 257 | } else { 258 | r0 = ret.Error(0) 259 | } 260 | 261 | return r0 262 | } 263 | 264 | // LinkSetVfVlanQosProto provides a mock function with given fields: _a0, _a1, _a2, _a3, _a4 265 | func (_m *NetlinkManager) LinkSetVfVlanQosProto(_a0 netlink.Link, _a1 int, _a2 int, _a3 int, _a4 int) error { 266 | ret := _m.Called(_a0, _a1, _a2, _a3, _a4) 267 | 268 | if len(ret) == 0 { 269 | panic("no return value specified for LinkSetVfVlanQosProto") 270 | } 271 | 272 | var r0 error 273 | if rf, ok := ret.Get(0).(func(netlink.Link, int, int, int, int) error); ok { 274 | r0 = rf(_a0, _a1, _a2, _a3, _a4) 275 | } else { 276 | r0 = ret.Error(0) 277 | } 278 | 279 | return r0 280 | } 281 | 282 | // NewNetlinkManager creates a new instance of NetlinkManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 283 | // The first argument is typically a *testing.T value. 284 | func NewNetlinkManager(t interface { 285 | mock.TestingT 286 | Cleanup(func()) 287 | }) *NetlinkManager { 288 | mock := &NetlinkManager{} 289 | mock.Mock.Test(t) 290 | 291 | t.Cleanup(func() { mock.AssertExpectations(t) }) 292 | 293 | return mock 294 | } 295 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/containernetworking/plugins/pkg/testutils" 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | 27 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/types" 28 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils" 29 | ) 30 | 31 | var _ = Describe("Config", func() { 32 | BeforeEach(func() { 33 | DeferCleanup(func(x string) { DefaultCNIDir = x }, DefaultCNIDir) 34 | DefaultCNIDir = GinkgoT().TempDir() 35 | }) 36 | 37 | Context("Checking LoadConf function", func() { 38 | It("Assuming correct config file - existing DeviceID", func() { 39 | conf := []byte(`{ 40 | "name": "mynet", 41 | "type": "sriov", 42 | "deviceID": "0000:af:06.1", 43 | "vf": 0, 44 | "ipam": { 45 | "type": "host-local", 46 | "subnet": "10.55.206.0/26", 47 | "routes": [ 48 | { "dst": "0.0.0.0/0" } 49 | ], 50 | "gateway": "10.55.206.1" 51 | } 52 | }`) 53 | _, err := LoadConf(conf) 54 | Expect(err).NotTo(HaveOccurred()) 55 | }) 56 | It("Assuming incorrect config file - not existing DeviceID", func() { 57 | conf := []byte(`{ 58 | "name": "mynet", 59 | "type": "sriov", 60 | "deviceID": "0000:af:06.3", 61 | "vf": 0, 62 | "ipam": { 63 | "type": "host-local", 64 | "subnet": "10.55.206.0/26", 65 | "routes": [ 66 | { "dst": "0.0.0.0/0" } 67 | ], 68 | "gateway": "10.55.206.1" 69 | } 70 | }`) 71 | _, err := LoadConf(conf) 72 | Expect(err).To(HaveOccurred()) 73 | }) 74 | It("Assuming incorrect config file - broken json", func() { 75 | conf := []byte(`{ 76 | "name": "mynet" 77 | "type": "sriov", 78 | "deviceID": "0000:af:06.1", 79 | "vf": 0, 80 | "ipam": { 81 | "type": "host-local", 82 | "subnet": "10.55.206.0/26", 83 | "routes": [ 84 | { "dst": "0.0.0.0/0" } 85 | ], 86 | "gateway": "10.55.206.1" 87 | } 88 | }`) 89 | _, err := LoadConf(conf) 90 | Expect(err).To(HaveOccurred()) 91 | }) 92 | 93 | validVlanID := 100 94 | zeroVlanID := 0 95 | invalidVlanID := 5000 96 | validQoS := 1 97 | zeroQoS := 0 98 | invalidQoS := 10 99 | valid8021qProto := "802.1Q" 100 | valid8021adProto := "802.1ad" 101 | invalidProto := "802" 102 | DescribeTable("Vlan ID, QoS and Proto", 103 | func(vlanID *int, vlanQoS *int, vlanProto *string, failure bool) { 104 | 105 | s := `{ 106 | "name": "mynet", 107 | "type": "sriov", 108 | "deviceID": "0000:af:06.1", 109 | "vf": 0` 110 | if vlanID != nil { 111 | s = fmt.Sprintf(`%s, 112 | "vlan": %d`, s, *vlanID) 113 | } 114 | if vlanQoS != nil { 115 | s = fmt.Sprintf(`%s, 116 | "vlanQoS": %d`, s, *vlanQoS) 117 | } 118 | if vlanProto != nil { 119 | s = fmt.Sprintf(`%s, 120 | "vlanProto": "%s"`, s, *vlanProto) 121 | } 122 | s = fmt.Sprintf(`%s 123 | }`, s) 124 | conf := []byte(s) 125 | _, err := LoadConf(conf) 126 | if failure { 127 | Expect(err).To(HaveOccurred()) 128 | } else { 129 | Expect(err).ToNot(HaveOccurred()) 130 | } 131 | }, 132 | Entry("valid vlan ID", &validVlanID, nil, nil, false), 133 | Entry("invalid vlan ID", &invalidVlanID, nil, nil, true), 134 | Entry("vlan ID equal to zero and non-zero QoS set", &zeroVlanID, &validQoS, nil, true), 135 | Entry("vlan ID equal to zero and 802.1ad Proto set", &zeroVlanID, nil, &valid8021adProto, true), 136 | Entry("invalid QoS", &validVlanID, &invalidQoS, nil, true), 137 | Entry("invalid Proto", &validVlanID, nil, &invalidProto, true), 138 | Entry("valid 802.1q Proto", &validVlanID, nil, &valid8021qProto, false), 139 | Entry("valid 802.1ad Proto", &validVlanID, nil, &valid8021adProto, false), 140 | Entry("no vlan ID and non-zero QoS set", nil, &validQoS, nil, true), 141 | Entry("no vlan ID and 802.1ad Proto set", nil, nil, &valid8021adProto, true), 142 | Entry("default values for vlan, qos and proto", &zeroVlanID, &zeroQoS, &valid8021qProto, false), 143 | ) 144 | 145 | It("Assuming device is allocated", func() { 146 | conf := []byte(`{ 147 | "name": "mynet", 148 | "type": "sriov", 149 | "deviceID": "0000:af:06.1", 150 | "vf": 0, 151 | "ipam": { 152 | "type": "host-local", 153 | "subnet": "10.55.206.0/26", 154 | "routes": [ 155 | { "dst": "0.0.0.0/0" } 156 | ], 157 | "gateway": "10.55.206.1" 158 | } 159 | }`) 160 | 161 | tmpdir, err := os.MkdirTemp("/tmp", "sriovplugin-testfiles-") 162 | Expect(err).ToNot(HaveOccurred()) 163 | originCNIDir := DefaultCNIDir 164 | DefaultCNIDir = tmpdir 165 | defer func() { 166 | DefaultCNIDir = originCNIDir 167 | }() 168 | 169 | targetNetNS, err := testutils.NewNS() 170 | Expect(err).NotTo(HaveOccurred()) 171 | defer func() { 172 | if targetNetNS != nil { 173 | targetNetNS.Close() 174 | err = testutils.UnmountNS(targetNetNS) 175 | } 176 | }() 177 | 178 | allocator := utils.NewPCIAllocator(tmpdir) 179 | err = allocator.SaveAllocatedPCI("0000:af:06.1", targetNetNS.Path()) 180 | Expect(err).ToNot(HaveOccurred()) 181 | 182 | _, err = LoadConf(conf) 183 | Expect(err).To(HaveOccurred()) 184 | Expect(err.Error()).To(ContainSubstring("pci address 0000:af:06.1 is already allocated")) 185 | }) 186 | 187 | }) 188 | Context("Checking getVfInfo function", func() { 189 | It("Assuming existing PF", func() { 190 | _, _, err := getVfInfo("0000:af:06.0") 191 | Expect(err).NotTo(HaveOccurred()) 192 | }) 193 | It("Assuming not existing PF", func() { 194 | _, _, err := getVfInfo("0000:af:07.0") 195 | Expect(err).To(HaveOccurred()) 196 | }) 197 | }) 198 | Context("Checking GetMacAddressForResult function", func() { 199 | It("Should return the mac address requested by the user", func() { 200 | netconf := &types.NetConf{SriovNetConf: types.SriovNetConf{ 201 | MAC: "MAC", 202 | OrigVfState: types.VfState{ 203 | EffectiveMAC: "EffectiveMAC", 204 | AdminMAC: "AdminMAC", 205 | }}, 206 | } 207 | 208 | Expect(GetMacAddressForResult(netconf)).To(Equal("MAC")) 209 | }) 210 | It("Should return the EffectiveMAC mac address if the user didn't request and the the driver is not DPDK", func() { 211 | netconf := &types.NetConf{SriovNetConf: types.SriovNetConf{ 212 | DPDKMode: false, 213 | OrigVfState: types.VfState{ 214 | EffectiveMAC: "EffectiveMAC", 215 | AdminMAC: "AdminMAC", 216 | }}, 217 | } 218 | 219 | Expect(GetMacAddressForResult(netconf)).To(Equal("EffectiveMAC")) 220 | }) 221 | It("Should return the AdminMAC mac address if the user didn't request and the the driver is DPDK", func() { 222 | netconf := &types.NetConf{SriovNetConf: types.SriovNetConf{ 223 | DPDKMode: true, 224 | OrigVfState: types.VfState{ 225 | EffectiveMAC: "EffectiveMAC", 226 | AdminMAC: "AdminMAC", 227 | }}, 228 | } 229 | 230 | Expect(GetMacAddressForResult(netconf)).To(Equal("AdminMAC")) 231 | }) 232 | It("Should return empty string if the user didn't request the the driver is DPDK and adminMac is 0", func() { 233 | netconf := &types.NetConf{SriovNetConf: types.SriovNetConf{ 234 | DPDKMode: true, 235 | OrigVfState: types.VfState{ 236 | AdminMAC: "00:00:00:00:00:00", 237 | }}, 238 | } 239 | 240 | Expect(GetMacAddressForResult(netconf)).To(Equal("")) 241 | }) 242 | }) 243 | }) 244 | -------------------------------------------------------------------------------- /pkg/logging/logging_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package logging 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | 24 | g "github.com/onsi/ginkgo/v2" 25 | o "github.com/onsi/gomega" 26 | ) 27 | 28 | var _ = g.Describe("Logging", func() { 29 | var origStderr *os.File 30 | var stderrFile *os.File 31 | 32 | g.BeforeEach(func() { 33 | var err error 34 | stderrFile, err = os.CreateTemp("", "") 35 | o.Expect(err).NotTo(o.HaveOccurred()) 36 | origStderr = os.Stderr 37 | os.Stderr = stderrFile 38 | }) 39 | 40 | g.AfterEach(func() { 41 | os.Stderr = origStderr 42 | o.Expect(stderrFile.Close()).To(o.Succeed()) 43 | o.Expect(os.RemoveAll(stderrFile.Name())).To(o.Succeed()) 44 | }) 45 | 46 | g.Context("log argument prepender", func() { 47 | g.When("none of netns, containerID, ifName are specified", func() { 48 | g.BeforeEach(func() { 49 | Init("", "", "", "", "") 50 | }) 51 | 52 | g.It("should only prepend the cniName", func() { 53 | Panic("test message", "a", "b") 54 | _, _ = stderrFile.Seek(0, 0) 55 | out, err := io.ReadAll(stderrFile) 56 | o.Expect(err).NotTo(o.HaveOccurred()) 57 | //nolint:gocritic 58 | o.Expect(out).Should(o.ContainSubstring(fmt.Sprintf(`%s="%s"`, labelCNIName, cniName))) 59 | o.Expect(out).ShouldNot(o.ContainSubstring(labelContainerID)) 60 | o.Expect(out).ShouldNot(o.ContainSubstring(labelNetNS)) 61 | o.Expect(out).ShouldNot(o.ContainSubstring(labelIFName)) 62 | }) 63 | }) 64 | 65 | g.When("netns, containerID and ifName are specified", func() { 66 | const ( 67 | testContainerID = "test-containerid" 68 | testNetNS = "test-netns" 69 | testIFName = "test-ifname" 70 | ) 71 | 72 | g.BeforeEach(func() { 73 | Init("", "", testContainerID, testNetNS, testIFName) 74 | }) 75 | 76 | g.It("should log cniName, netns, containerID and ifName", func() { 77 | Panic("test message", "a", "b") 78 | _, _ = stderrFile.Seek(0, 0) 79 | out, err := io.ReadAll(stderrFile) 80 | o.Expect(err).NotTo(o.HaveOccurred()) 81 | //nolint:gocritic 82 | o.Expect(out).Should(o.ContainSubstring(fmt.Sprintf(`%s="%s"`, labelCNIName, cniName))) 83 | //nolint:gocritic 84 | o.Expect(out).Should(o.ContainSubstring(fmt.Sprintf(`%s="%s"`, labelContainerID, testContainerID))) 85 | //nolint:gocritic 86 | o.Expect(out).Should(o.ContainSubstring(fmt.Sprintf(`%s="%s"`, labelNetNS, testNetNS))) 87 | //nolint:gocritic 88 | o.Expect(out).Should(o.ContainSubstring(fmt.Sprintf(`%s="%s"`, labelIFName, testIFName))) 89 | }) 90 | }) 91 | }) 92 | 93 | g.Context("log levels", func() { 94 | g.When("the defaults are used", func() { 95 | g.BeforeEach(func() { 96 | Init("", "", "", "", "") 97 | }) 98 | 99 | g.It("panic messages are logged to stderr", func() { 100 | Panic("test message", "a", "b") 101 | _, _ = stderrFile.Seek(0, 0) 102 | out, err := io.ReadAll(stderrFile) 103 | o.Expect(err).NotTo(o.HaveOccurred()) 104 | o.Expect(out).Should(o.ContainSubstring("test message")) 105 | }) 106 | 107 | g.It("info messages are logged to stderr and look as expected", func() { 108 | Info("test message", "a", "b") 109 | _, _ = stderrFile.Seek(0, 0) 110 | out, err := io.ReadAll(stderrFile) 111 | o.Expect(err).NotTo(o.HaveOccurred()) 112 | o.Expect(out).Should(o.ContainSubstring(`msg="test message"`)) 113 | o.Expect(out).Should(o.ContainSubstring(`a="b"`)) 114 | o.Expect(out).Should(o.ContainSubstring(`level="info"`)) 115 | }) 116 | 117 | g.It("debug messages are not logged to stderr", func() { 118 | Debug("test message", "a", "b") 119 | _, _ = stderrFile.Seek(0, 0) 120 | out, err := io.ReadAll(stderrFile) 121 | o.Expect(err).NotTo(o.HaveOccurred()) 122 | o.Expect(out).ShouldNot(o.ContainSubstring("test message")) 123 | }) 124 | }) 125 | 126 | g.When("the log level is raised to warning", func() { 127 | g.BeforeEach(func() { 128 | Init("warning", "", "", "", "") 129 | }) 130 | 131 | g.It("panic messages are logged to stderr", func() { 132 | Panic("test message", "a", "b") 133 | _, _ = stderrFile.Seek(0, 0) 134 | out, err := io.ReadAll(stderrFile) 135 | o.Expect(err).NotTo(o.HaveOccurred()) 136 | o.Expect(out).Should(o.ContainSubstring("test message")) 137 | }) 138 | 139 | g.It("error messages are logged to stderr", func() { 140 | Error("test message", "a", "b") 141 | _, _ = stderrFile.Seek(0, 0) 142 | out, err := io.ReadAll(stderrFile) 143 | o.Expect(err).NotTo(o.HaveOccurred()) 144 | o.Expect(out).Should(o.ContainSubstring("test message")) 145 | }) 146 | 147 | g.It("warning messages are logged to stderr", func() { 148 | Warning("test message", "a", "b") 149 | _, _ = stderrFile.Seek(0, 0) 150 | out, err := io.ReadAll(stderrFile) 151 | o.Expect(err).NotTo(o.HaveOccurred()) 152 | o.Expect(out).Should(o.ContainSubstring("test message")) 153 | }) 154 | 155 | g.It("info messages are not logged to stderr", func() { 156 | Info("test message", "a", "b") 157 | _, _ = stderrFile.Seek(0, 0) 158 | out, err := io.ReadAll(stderrFile) 159 | o.Expect(err).NotTo(o.HaveOccurred()) 160 | o.Expect(out).ShouldNot(o.ContainSubstring("test message")) 161 | }) 162 | }) 163 | 164 | g.When("the log level is set to an invalid value", func() { 165 | g.BeforeEach(func() { 166 | Init("I'm invalid", "", "", "", "") 167 | }) 168 | 169 | g.It("panic messages are logged to stderr", func() { 170 | Panic("test message", "a", "b") 171 | _, _ = stderrFile.Seek(0, 0) 172 | out, err := io.ReadAll(stderrFile) 173 | o.Expect(err).NotTo(o.HaveOccurred()) 174 | o.Expect(out).Should(o.ContainSubstring("test message")) 175 | }) 176 | 177 | g.It("info messages are logged to stderr", func() { 178 | Info("test message", "a", "b") 179 | _, _ = stderrFile.Seek(0, 0) 180 | out, err := io.ReadAll(stderrFile) 181 | o.Expect(err).NotTo(o.HaveOccurred()) 182 | o.Expect(out).Should(o.ContainSubstring("test message")) 183 | }) 184 | 185 | g.It("debug messages are not logged to stderr", func() { 186 | Debug("test message", "a", "b") 187 | _, _ = stderrFile.Seek(0, 0) 188 | out, err := io.ReadAll(stderrFile) 189 | o.Expect(err).NotTo(o.HaveOccurred()) 190 | o.Expect(out).ShouldNot(o.ContainSubstring("test message")) 191 | }) 192 | }) 193 | }) 194 | 195 | g.Context("log files", func() { 196 | var logFile *os.File 197 | 198 | g.BeforeEach(func() { 199 | var err error 200 | logFile, err = os.CreateTemp("", "") 201 | o.Expect(err).NotTo(o.HaveOccurred()) 202 | }) 203 | 204 | g.AfterEach(func() { 205 | o.Expect(logFile.Close()).To(o.Succeed()) 206 | o.Expect(os.RemoveAll(logFile.Name())).To(o.Succeed()) 207 | }) 208 | 209 | g.When("the log file is set", func() { 210 | g.BeforeEach(func() { 211 | Init("", logFile.Name(), "", "", "") 212 | }) 213 | 214 | g.It("error messages are logged to log file but not to stderr", func() { 215 | Error("test message", "a", "b") 216 | _, _ = stderrFile.Seek(0, 0) 217 | out, err := io.ReadAll(logFile) 218 | o.Expect(err).NotTo(o.HaveOccurred()) 219 | o.Expect(out).Should(o.ContainSubstring("test message")) 220 | 221 | _, _ = stderrFile.Seek(0, 0) 222 | out, err = io.ReadAll(stderrFile) 223 | o.Expect(err).NotTo(o.HaveOccurred()) 224 | o.Expect(out).ShouldNot(o.ContainSubstring("test message")) 225 | }) 226 | }) 227 | 228 | g.When("the log file is set and then unset", func() { 229 | g.BeforeEach(func() { 230 | // TODO: This triggers a data race in github.com/k8snetworkplumbingwg/cni-log; fix the datarace in the 231 | // logging component and then remove the skip. 232 | g.Skip("https://github.com/k8snetworkplumbingwg/cni-log/issues/15") 233 | Init("", logFile.Name(), "", "", "") 234 | setLogFile("") 235 | }) 236 | 237 | g.It("logs to stderr but not to file", func() { 238 | Error("test message", "a", "b") 239 | _, _ = stderrFile.Seek(0, 0) 240 | out, err := io.ReadAll(logFile) 241 | o.Expect(err).NotTo(o.HaveOccurred()) 242 | o.Expect(out).ShouldNot(o.ContainSubstring("test message")) 243 | 244 | _, _ = stderrFile.Seek(0, 0) 245 | out, err = io.ReadAll(stderrFile) 246 | o.Expect(err).NotTo(o.HaveOccurred()) 247 | o.Expect(out).Should(o.ContainSubstring("test message")) 248 | }) 249 | }) 250 | }) 251 | }) 252 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "net" 23 | "os" 24 | "path/filepath" 25 | "time" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | 30 | "github.com/vishvananda/netlink" 31 | 32 | cnitypes "github.com/containernetworking/cni/pkg/types" 33 | 34 | sriovtypes "github.com/k8snetworkplumbingwg/sriov-cni/pkg/types" 35 | mocks_utils "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils/mocks" 36 | ) 37 | 38 | var _ = Describe("Utils", func() { 39 | 40 | Context("Checking GetSriovNumVfs function", func() { 41 | It("Assuming existing interface", func() { 42 | result, err := GetSriovNumVfs("enp175s0f1") 43 | Expect(result).To(Equal(2), "Existing sriov interface should return correct VFs count") 44 | Expect(err).NotTo(HaveOccurred(), "Existing sriov interface should not return an error") 45 | }) 46 | It("Assuming not existing interface", func() { 47 | _, err := GetSriovNumVfs("enp175s0f2") 48 | Expect(err).To(HaveOccurred(), "Not existing sriov interface should return an error") 49 | }) 50 | }) 51 | Context("Checking GetVfid function", func() { 52 | It("Assuming existing interface", func() { 53 | result, err := GetVfid("0000:af:06.0", "enp175s0f1") 54 | Expect(result).To(Equal(0), "Existing VF should return correct VF index") 55 | Expect(err).NotTo(HaveOccurred(), "Existing VF should not return an error") 56 | }) 57 | It("Assuming not existing interface", func() { 58 | _, err := GetVfid("0000:af:06.0", "enp175s0f2") 59 | Expect(err).To(HaveOccurred(), "Not existing interface should return an error") 60 | }) 61 | }) 62 | Context("Checking GetPfName function", func() { 63 | It("Assuming existing vf", func() { 64 | result, err := GetPfName("0000:af:06.0") 65 | Expect(err).NotTo(HaveOccurred(), "Existing VF should not return an error") 66 | Expect(result).To(Equal("enp175s0f1"), "Existing VF should return correct PF name") 67 | }) 68 | It("Assuming not existing vf", func() { 69 | result, err := GetPfName("0000:af:07.0") 70 | Expect(result).To(Equal("")) 71 | Expect(err).To(HaveOccurred(), "Not existing VF should return an error") 72 | }) 73 | }) 74 | Context("Checking GetPciAddress function", func() { 75 | It("Assuming existing interface and vf", func() { 76 | Expect(GetPciAddress("enp175s0f1", 0)).To(Equal("0000:af:06.0"), "Existing PF and VF id should return correct VF pci address") 77 | }) 78 | It("Assuming not existing interface", func() { 79 | _, err := GetPciAddress("enp175s0f2", 0) 80 | Expect(err).To(HaveOccurred(), "Not existing PF should return an error") 81 | }) 82 | It("Assuming not existing vf", func() { 83 | result, err := GetPciAddress("enp175s0f1", 33) 84 | Expect(result).To(Equal(""), "Not existing VF id should not return pci address") 85 | Expect(err).To(HaveOccurred(), "Not existing VF id should return an error") 86 | }) 87 | }) 88 | Context("Checking GetSharedPF function", func() { 89 | /* TO-DO */ 90 | // It("Assuming existing interface", func() { 91 | // result, err := GetSharedPF("enp175s0f1") 92 | // Expect(result).To(Equal("sharedpf"), "Looking for shared PF for supported NIC should return correct PF name") 93 | // Expect(err).NotTo(HaveOccurred(), "Looking for shared PF for supported NIC should not return an error") 94 | // }) 95 | // It("Assuming not existing interface", func() { 96 | // _, err := GetSharedPF("enp175s0f2") 97 | // Expect(err).To(HaveOccurred(), "Looking for shared PF for not supported NIC should return an error") 98 | // }) 99 | }) 100 | Context("Checking GetVFLinkName function", func() { 101 | It("Assuming existing vf", func() { 102 | result, err := GetVFLinkNamesFromVFID("enp175s0f1", 0) 103 | Expect(result).To(ContainElement("enp175s6"), "Existing PF should have at least one VF") 104 | Expect(err).NotTo(HaveOccurred(), "Existing PF should not return an error") 105 | }) 106 | It("Assuming not existing vf", func() { 107 | _, err := GetVFLinkNamesFromVFID("enp175s0f1", 3) 108 | Expect(err).To(HaveOccurred(), "Not existing VF should return an error") 109 | }) 110 | }) 111 | Context("Checking Retry function", func() { 112 | It("Assuming calling function fails", func() { 113 | err := Retry(5, 10*time.Millisecond, func() error { return errors.New("") }) 114 | Expect(err).To((HaveOccurred()), "Retry should return an error") 115 | }) 116 | It("Assuming calling function does not fail", func() { 117 | err := Retry(5, 10*time.Millisecond, func() error { return nil }) 118 | Expect(err).NotTo((HaveOccurred()), "Retry should not return an error") 119 | }) 120 | }) 121 | Context("Checking SetVFEffectiveMAC function", func() { 122 | It("assuming calling function fails", func() { 123 | mocked := &mocks_utils.NetlinkManager{} 124 | fakeMac, err := net.ParseMAC("6e:16:06:0e:b7:e9") 125 | Expect(err).ToNot(HaveOccurred()) 126 | fakeNewMac, err := net.ParseMAC("60:00:00:00:00:01") 127 | Expect(err).ToNot(HaveOccurred()) 128 | 129 | fakeLink := &FakeLink{netlink.LinkAttrs{ 130 | Index: 1000, 131 | Name: "enp175s0f1", 132 | HardwareAddr: fakeMac, 133 | }} 134 | 135 | mocked.On("LinkByName", "enp175s0f1").Return(fakeLink, nil) 136 | mocked.On("LinkSetHardwareAddr", fakeLink, fakeNewMac).Return(nil) 137 | 138 | err = SetVFEffectiveMAC(mocked, "enp175s0f1", "60:00:00:00:00:01") 139 | Expect(err).To(HaveOccurred()) 140 | Expect(err.Error()).To(ContainSubstring("effective mac address is different from requested one")) 141 | }) 142 | 143 | It("assuming calling function does not fails", func() { 144 | mocked := &mocks_utils.NetlinkManager{} 145 | fakeMac, err := net.ParseMAC("60:00:00:00:00:01") 146 | Expect(err).ToNot(HaveOccurred()) 147 | fakeNewMac, err := net.ParseMAC("60:00:00:00:00:01") 148 | Expect(err).ToNot(HaveOccurred()) 149 | 150 | fakeLink := &FakeLink{netlink.LinkAttrs{ 151 | Index: 1000, 152 | Name: "enp175s0f1", 153 | HardwareAddr: fakeMac, 154 | }} 155 | 156 | mocked.On("LinkByName", "enp175s0f1").Return(fakeLink, nil) 157 | mocked.On("LinkSetHardwareAddr", fakeLink, fakeNewMac).Return(nil) 158 | 159 | err = SetVFEffectiveMAC(mocked, "enp175s0f1", "60:00:00:00:00:01") 160 | Expect(err).ToNot(HaveOccurred()) 161 | }) 162 | }) 163 | Context("Checking SetVFHardwareMAC function", func() { 164 | It("assuming calling function fails", func() { 165 | mocked := &mocks_utils.NetlinkManager{} 166 | fakeMac, err := net.ParseMAC("6e:16:06:0e:b7:e9") 167 | Expect(err).ToNot(HaveOccurred()) 168 | fakeNewMac, err := net.ParseMAC("60:00:00:00:00:01") 169 | Expect(err).ToNot(HaveOccurred()) 170 | 171 | fakeLink := &FakeLink{netlink.LinkAttrs{ 172 | Index: 1000, 173 | Name: "enp175s0f1", 174 | Vfs: []netlink.VfInfo{ 175 | {Mac: fakeMac}, 176 | }, 177 | }} 178 | 179 | mocked.On("LinkByName", "enp175s0f1").Return(fakeLink, nil) 180 | mocked.On("LinkSetVfHardwareAddr", fakeLink, 0, fakeNewMac).Return(nil) 181 | 182 | err = SetVFHardwareMAC(mocked, "enp175s0f1", 0, "60:00:00:00:00:01") 183 | Expect(err).To(HaveOccurred()) 184 | Expect(err.Error()).To(ContainSubstring("hardware mac address is different from requested one")) 185 | }) 186 | 187 | It("assuming calling function does not fails", func() { 188 | mocked := &mocks_utils.NetlinkManager{} 189 | fakeMac, err := net.ParseMAC("60:00:00:00:00:01") 190 | Expect(err).ToNot(HaveOccurred()) 191 | fakeNewMac, err := net.ParseMAC("60:00:00:00:00:01") 192 | Expect(err).ToNot(HaveOccurred()) 193 | 194 | fakeLink := &FakeLink{netlink.LinkAttrs{ 195 | Index: 1000, 196 | Name: "enp175s0f1", 197 | Vfs: []netlink.VfInfo{ 198 | {Mac: fakeMac}, 199 | }, 200 | }} 201 | 202 | mocked.On("LinkByName", "enp175s0f1").Return(fakeLink, nil) 203 | mocked.On("LinkSetVfHardwareAddr", fakeLink, 0, fakeNewMac).Return(nil) 204 | 205 | err = SetVFHardwareMAC(mocked, "enp175s0f1", 0, "60:00:00:00:00:01") 206 | Expect(err).ToNot(HaveOccurred()) 207 | }) 208 | }) 209 | 210 | Context("Checking SaveNetConf function", func() { 211 | var tmpDir string 212 | 213 | BeforeEach(func() { 214 | var err error 215 | tmpDir, err = os.MkdirTemp("", "sriov") 216 | Expect(err).ToNot(HaveOccurred()) 217 | }) 218 | It("should save all the netConf struct to the cache file without dns", func() { 219 | netconf := &sriovtypes.NetConf{NetConf: cnitypes.NetConf{CNIVersion: "1.0.0"}, SriovNetConf: sriovtypes.SriovNetConf{DeviceID: "0000:af:06.0"}} 220 | err := SaveNetConf("test", tmpDir, "net1", netconf) 221 | Expect(err).ToNot(HaveOccurred()) 222 | 223 | data, err := os.ReadFile(filepath.Join(tmpDir, "test-net1")) 224 | Expect(err).ToNot(HaveOccurred()) 225 | Expect(data).ToNot(ContainSubstring("dns")) 226 | 227 | newNetConf := &sriovtypes.NetConf{} 228 | err = json.Unmarshal(data, newNetConf) 229 | Expect(err).ToNot(HaveOccurred()) 230 | 231 | Expect(netconf.DeviceID).To(Equal(newNetConf.DeviceID)) 232 | Expect(netconf.CNIVersion).To(Equal(newNetConf.CNIVersion)) 233 | }) 234 | It("should save all the netConf struct to the cache file with dns", func() { 235 | netconf := sriovtypes.NetConf{NetConf: cnitypes.NetConf{CNIVersion: "1.0.0", DNS: cnitypes.DNS{Domain: "bla"}}, SriovNetConf: sriovtypes.SriovNetConf{DeviceID: "0000:af:06.0"}} 236 | err := SaveNetConf("test", tmpDir, "net1", &netconf) 237 | Expect(err).ToNot(HaveOccurred()) 238 | 239 | data, err := os.ReadFile(filepath.Join(tmpDir, "test-net1")) 240 | Expect(err).ToNot(HaveOccurred()) 241 | Expect(data).To(ContainSubstring("dns")) 242 | 243 | newNetConf := &sriovtypes.NetConf{} 244 | err = json.Unmarshal(data, newNetConf) 245 | Expect(err).ToNot(HaveOccurred()) 246 | 247 | Expect(netconf.DeviceID).To(Equal(newNetConf.DeviceID)) 248 | Expect(netconf.CNIVersion).To(Equal(newNetConf.CNIVersion)) 249 | Expect(netconf.DNS.Domain).To(Equal(newNetConf.DNS.Domain)) 250 | }) 251 | }) 252 | }) 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /pkg/utils/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | /* 18 | This file contains test helper functions to mock linux sysfs directory. 19 | If a package need to access system sysfs it should call CreateTmpSysFs() before test 20 | then call RemoveTmpSysFs() once test is done for clean up. 21 | */ 22 | 23 | package utils 24 | 25 | import ( 26 | "fmt" 27 | "log" 28 | "math" 29 | "net" 30 | "os" 31 | "path/filepath" 32 | "syscall" 33 | 34 | "github.com/vishvananda/netlink" 35 | ) 36 | 37 | type tmpSysFs struct { 38 | dirRoot string 39 | dirList []string 40 | fileList map[string][]byte 41 | netSymlinks map[string]string 42 | devSymlinks map[string]string 43 | vfSymlinks map[string]string 44 | originalRoot *os.File 45 | } 46 | 47 | var ts = tmpSysFs{ 48 | dirList: []string{ 49 | "sys/class/net", 50 | "sys/bus/pci/devices", 51 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1/net/enp175s0f1", 52 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.0/net/enp175s6", 53 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.1/net/enp175s7", 54 | "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0/net/ens1", 55 | "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0/net/ens1d1", 56 | }, 57 | fileList: map[string][]byte{ 58 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1/sriov_numvfs": []byte("2"), 59 | "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0/sriov_numvfs": []byte("0"), 60 | }, 61 | netSymlinks: map[string]string{ 62 | "sys/class/net/enp175s0f1": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1/net/enp175s0f1", 63 | "sys/class/net/enp175s6": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.0/net/enp175s6", 64 | "sys/class/net/enp175s7": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.1/net/enp175s7", 65 | "sys/class/net/ens1": "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0/net/ens1", 66 | "sys/class/net/ens1d1": "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0/net/ens1d1", 67 | }, 68 | devSymlinks: map[string]string{ 69 | "sys/class/net/enp175s0f1/device": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1", 70 | "sys/class/net/enp175s6/device": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.0", 71 | "sys/class/net/enp175s7/device": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.1", 72 | "sys/class/net/ens1/device": "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0", 73 | "sys/class/net/ens1d1/device": "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0", 74 | 75 | "sys/bus/pci/devices/0000:af:00.1": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1", 76 | "sys/bus/pci/devices/0000:af:06.0": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.0", 77 | "sys/bus/pci/devices/0000:af:06.1": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.1", 78 | "sys/bus/pci/devices/0000:05:00.0": "sys/devices/pci0000:00/0000:00:02.0/0000:05:00.0", 79 | }, 80 | vfSymlinks: map[string]string{ 81 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1/virtfn0": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.0", 82 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.0/physfn": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1", 83 | 84 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1/virtfn1": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.1", 85 | "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:06.1/physfn": "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1", 86 | }, 87 | } 88 | 89 | // CreateTmpSysFs create mock sysfs for testing 90 | func CreateTmpSysFs() error { 91 | originalRoot, _ := os.Open("/") 92 | ts.originalRoot = originalRoot 93 | 94 | tmpdir, err := os.MkdirTemp("/tmp", "sriovplugin-testfiles-") 95 | if err != nil { 96 | return err 97 | } 98 | 99 | ts.dirRoot = tmpdir 100 | 101 | for _, dir := range ts.dirList { 102 | if err := os.MkdirAll(filepath.Join(ts.dirRoot, dir), 0755); err != nil { 103 | return err 104 | } 105 | } 106 | 107 | for filename, body := range ts.fileList { 108 | if err := os.WriteFile(filepath.Join(ts.dirRoot, filename), body, 0600); err != nil { 109 | return err 110 | } 111 | } 112 | 113 | for link, target := range ts.netSymlinks { 114 | if err := createSymlinks(filepath.Join(ts.dirRoot, link), filepath.Join(ts.dirRoot, target)); err != nil { 115 | return err 116 | } 117 | } 118 | 119 | for link, target := range ts.devSymlinks { 120 | if err := createSymlinks(filepath.Join(ts.dirRoot, link), filepath.Join(ts.dirRoot, target)); err != nil { 121 | return err 122 | } 123 | } 124 | 125 | for link, target := range ts.vfSymlinks { 126 | if err := createSymlinks(filepath.Join(ts.dirRoot, link), filepath.Join(ts.dirRoot, target)); err != nil { 127 | return err 128 | } 129 | } 130 | 131 | SysBusPci = filepath.Join(ts.dirRoot, SysBusPci) 132 | NetDirectory = filepath.Join(ts.dirRoot, NetDirectory) 133 | return nil 134 | } 135 | 136 | func createSymlinks(link, target string) error { 137 | if err := os.MkdirAll(target, 0755); err != nil { 138 | return err 139 | } 140 | 141 | return os.Symlink(target, link) 142 | } 143 | 144 | // RemoveTmpSysFs removes mocked sysfs 145 | func RemoveTmpSysFs() error { 146 | if err := ts.originalRoot.Chdir(); err != nil { 147 | return err 148 | } 149 | if err := syscall.Chroot("."); err != nil { 150 | return err 151 | } 152 | if err := ts.originalRoot.Close(); err != nil { 153 | return err 154 | } 155 | 156 | return os.RemoveAll(ts.dirRoot) 157 | } 158 | 159 | // FakeLink is a dummy netlink struct used during testing 160 | type FakeLink struct { 161 | netlink.LinkAttrs 162 | } 163 | 164 | // type FakeLink struct { 165 | // linkAtrrs *netlink.LinkAttrs 166 | // } 167 | 168 | func (l *FakeLink) Attrs() *netlink.LinkAttrs { 169 | return &l.LinkAttrs 170 | } 171 | 172 | func (l *FakeLink) Type() string { 173 | return "FakeLink" 174 | } 175 | 176 | func MockNetlinkLib(methodCallRecordingDir string) func() { 177 | oldnetlinkLib := netLinkLib 178 | // see `ts` variable in this file 179 | // "sys/devices/pci0000:ae/0000:ae:00.0/0000:af:00.1/sriov_numvfs": []byte("2"), 180 | netLinkLib = newPFMockNetlinkLib(methodCallRecordingDir, "enp175s0f1", 2) 181 | 182 | return func() { 183 | netLinkLib = oldnetlinkLib 184 | } 185 | } 186 | 187 | // pfMockNetlinkLib creates dummy interfaces for Physical and Virtual functions, recording method calls on a log file in the form 188 | // ... 189 | type pfMockNetlinkLib struct { 190 | pf netlink.Link 191 | methodCallsRecordingFilePath string 192 | } 193 | 194 | func newPFMockNetlinkLib(recordDir, pfName string, numvfs int) *pfMockNetlinkLib { 195 | ret := &pfMockNetlinkLib{ 196 | pf: &netlink.Dummy{ 197 | LinkAttrs: netlink.LinkAttrs{ 198 | Name: pfName, 199 | Vfs: []netlink.VfInfo{}, 200 | }, 201 | }, 202 | } 203 | 204 | for i := 0; i < numvfs; i++ { 205 | ret.pf.Attrs().Vfs = append(ret.pf.Attrs().Vfs, netlink.VfInfo{ 206 | ID: i, 207 | Mac: mustParseMAC(fmt.Sprintf("ab:cd:ef:ab:cd:%02x", i)), 208 | }) 209 | } 210 | 211 | ret.methodCallsRecordingFilePath = filepath.Join(recordDir, pfName+".calls") 212 | 213 | ret.recordMethodCall("---") 214 | 215 | return ret 216 | } 217 | 218 | func (p *pfMockNetlinkLib) LinkByName(name string) (netlink.Link, error) { 219 | p.recordMethodCall("LinkByName %s", name) 220 | if name == p.pf.Attrs().Name { 221 | return p.pf, nil 222 | } 223 | return netlink.LinkByName(name) 224 | } 225 | 226 | func (p *pfMockNetlinkLib) LinkSetVfVlanQosProto(link netlink.Link, vfIndex, vlan, vlanQos, vlanProto int) error { 227 | p.recordMethodCall("LinkSetVfVlanQosProto %s %d %d %d %d", link.Attrs().Name, vfIndex, vlan, vlanQos, vlanProto) 228 | return nil 229 | } 230 | 231 | func (p *pfMockNetlinkLib) LinkSetVfHardwareAddr(pfLink netlink.Link, vfIndex int, hwaddr net.HardwareAddr) error { 232 | p.recordMethodCall("LinkSetVfHardwareAddr %s %d %s", pfLink.Attrs().Name, vfIndex, hwaddr.String()) 233 | pfLink.Attrs().Vfs[vfIndex].Mac = hwaddr 234 | return nil 235 | } 236 | 237 | func (p *pfMockNetlinkLib) LinkSetHardwareAddr(link netlink.Link, hwaddr net.HardwareAddr) error { 238 | p.recordMethodCall("LinkSetHardwareAddr %s %s", link.Attrs().Name, hwaddr.String()) 239 | return netlink.LinkSetHardwareAddr(link, hwaddr) 240 | } 241 | 242 | func (p *pfMockNetlinkLib) LinkSetUp(link netlink.Link) error { 243 | p.recordMethodCall("LinkSetUp %s", link.Attrs().Name) 244 | return netlink.LinkSetUp(link) 245 | } 246 | 247 | func (p *pfMockNetlinkLib) LinkSetDown(link netlink.Link) error { 248 | p.recordMethodCall("LinkSetDown %s", link.Attrs().Name) 249 | return netlink.LinkSetDown(link) 250 | } 251 | 252 | func (p *pfMockNetlinkLib) LinkSetNsFd(link netlink.Link, nsFd int) error { 253 | p.recordMethodCall("LinkSetNsFd %s %d", link.Attrs().Name, nsFd) 254 | return netlink.LinkSetNsFd(link, nsFd) 255 | } 256 | 257 | func (p *pfMockNetlinkLib) LinkSetName(link netlink.Link, name string) error { 258 | p.recordMethodCall("LinkSetName %s %s", link.Attrs().Name, name) 259 | link.Attrs().Name = name 260 | return netlink.LinkSetName(link, name) 261 | } 262 | 263 | //nolint:gosec 264 | func (p *pfMockNetlinkLib) LinkSetVfRate(pfLink netlink.Link, vfIndex, minRate, maxRate int) error { 265 | p.recordMethodCall("LinkSetVfRate %s %d %d %d", pfLink.Attrs().Name, vfIndex, minRate, maxRate) 266 | 267 | if maxRate > math.MaxUint32 { 268 | maxRate = math.MaxUint32 269 | } 270 | //nolint:gosec 271 | pfLink.Attrs().Vfs[vfIndex].MaxTxRate = uint32(maxRate) 272 | 273 | if minRate > math.MaxUint32 { 274 | minRate = math.MaxUint32 275 | } 276 | //nolint:gosec 277 | pfLink.Attrs().Vfs[vfIndex].MinTxRate = uint32(minRate) 278 | return nil 279 | } 280 | 281 | func (p *pfMockNetlinkLib) LinkSetVfSpoofchk(pfLink netlink.Link, vfIndex int, spoofChk bool) error { 282 | p.recordMethodCall("LinkSetVfRate %s %d %t", pfLink.Attrs().Name, vfIndex, spoofChk) 283 | pfLink.Attrs().Vfs[vfIndex].Spoofchk = spoofChk 284 | return nil 285 | } 286 | 287 | func (p *pfMockNetlinkLib) LinkSetVfTrust(pfLink netlink.Link, vfIndex int, trust bool) error { 288 | p.recordMethodCall("LinkSetVfTrust %s %d %d", pfLink.Attrs().Name, vfIndex, trust) 289 | if trust { 290 | pfLink.Attrs().Vfs[vfIndex].Trust = 1 291 | } else { 292 | pfLink.Attrs().Vfs[vfIndex].Trust = 0 293 | } 294 | 295 | return nil 296 | } 297 | 298 | func (p *pfMockNetlinkLib) LinkSetVfState(pfLink netlink.Link, vfIndex int, state uint32) error { 299 | p.recordMethodCall("LinkSetVfState %s %d %d", pfLink.Attrs().Name, vfIndex, state) 300 | pfLink.Attrs().Vfs[vfIndex].LinkState = state 301 | return nil 302 | } 303 | 304 | func (p *pfMockNetlinkLib) LinkSetMTU(link netlink.Link, mtu int) error { 305 | p.recordMethodCall("LinkSetMTU %s %d", link.Attrs().Name, mtu) 306 | return netlink.LinkSetMTU(link, mtu) 307 | } 308 | 309 | func (p *pfMockNetlinkLib) LinkDelAltName(link netlink.Link, name string) error { 310 | p.recordMethodCall("LinkDelAltName %s %s", link.Attrs().Name, name) 311 | return netlink.LinkDelAltName(link, name) 312 | } 313 | 314 | func (p *pfMockNetlinkLib) recordMethodCall(format string, a ...any) { 315 | message := fmt.Sprintf(format+"\n", a...) 316 | f, err := os.OpenFile(p.methodCallsRecordingFilePath, 317 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 318 | if err != nil { 319 | log.Printf("Can't open file to record method call [%s]: %v\n", message, err) 320 | return 321 | } 322 | defer f.Close() 323 | if _, err := f.WriteString(message); err != nil { 324 | log.Printf("Can't write on file [%s] to record method call [%s]: %v\n", p.methodCallsRecordingFilePath, message, err) 325 | } 326 | } 327 | 328 | func mustParseMAC(x string) net.HardwareAddr { 329 | ret, err := net.ParseMAC(x) 330 | if err != nil { 331 | panic(err) 332 | } 333 | return ret 334 | } 335 | -------------------------------------------------------------------------------- /pkg/cnicommands/cni.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package cnicommands 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "strings" 23 | "time" 24 | 25 | "github.com/containernetworking/cni/pkg/skel" 26 | "github.com/containernetworking/cni/pkg/types" 27 | current "github.com/containernetworking/cni/pkg/types/100" 28 | "github.com/containernetworking/plugins/pkg/ipam" 29 | "github.com/containernetworking/plugins/pkg/ns" 30 | "github.com/vishvananda/netlink" 31 | 32 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/config" 33 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/logging" 34 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/sriov" 35 | "github.com/k8snetworkplumbingwg/sriov-cni/pkg/utils" 36 | ) 37 | 38 | type envArgs struct { 39 | types.CommonArgs 40 | MAC types.UnmarshallableString `json:"mac,omitempty"` 41 | } 42 | 43 | func getEnvArgs(envArgsString string) (*envArgs, error) { 44 | if envArgsString != "" { 45 | e := envArgs{} 46 | err := types.LoadArgs(envArgsString, &e) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return &e, nil 51 | } 52 | return nil, nil 53 | } 54 | 55 | func CmdAdd(args *skel.CmdArgs) error { 56 | if err := config.SetLogging(args.StdinData, args.ContainerID, args.Netns, args.IfName); err != nil { 57 | return err 58 | } 59 | logging.Debug("function called", 60 | "func", "cmdAdd", 61 | "args.Path", args.Path, "args.StdinData", string(args.StdinData), "args.Args", args.Args) 62 | 63 | netConf, err := config.LoadConf(args.StdinData) 64 | if err != nil { 65 | return fmt.Errorf("SRIOV-CNI failed to load netconf: %v", err) 66 | } 67 | 68 | envArgs, err := getEnvArgs(args.Args) 69 | if err != nil { 70 | return fmt.Errorf("SRIOV-CNI failed to parse args: %v", err) 71 | } 72 | 73 | if envArgs != nil { 74 | MAC := string(envArgs.MAC) 75 | if MAC != "" { 76 | netConf.MAC = MAC 77 | } 78 | } 79 | 80 | // RuntimeConfig takes preference than envArgs. 81 | // This maintains compatibility of using envArgs 82 | // for MAC config. 83 | if netConf.RuntimeConfig.Mac != "" { 84 | netConf.MAC = netConf.RuntimeConfig.Mac 85 | } 86 | 87 | // Always use lower case for mac address 88 | netConf.MAC = strings.ToLower(netConf.MAC) 89 | 90 | netns, err := ns.GetNS(args.Netns) 91 | if err != nil { 92 | return fmt.Errorf("failed to open netns %q: %v", netns, err) 93 | } 94 | defer netns.Close() 95 | 96 | sm := sriov.NewSriovManager() 97 | err = sm.FillOriginalVfInfo(netConf) 98 | if err != nil { 99 | return fmt.Errorf("failed to get original vf information: %v", err) 100 | } 101 | defer func() { 102 | if err != nil { 103 | err := netns.Do(func(_ ns.NetNS) error { 104 | _, err := netlink.LinkByName(args.IfName) 105 | return err 106 | }) 107 | if err == nil { 108 | _ = sm.ReleaseVF(netConf, args.IfName, netns) 109 | } 110 | // Reset the VF if failure occurs before the netconf is cached 111 | _ = sm.ResetVFConfig(netConf) 112 | } 113 | }() 114 | if err := sm.ApplyVFConfig(netConf); err != nil { 115 | return fmt.Errorf("SRIOV-CNI failed to configure VF %q", err) 116 | } 117 | 118 | result := ¤t.Result{} 119 | result.Interfaces = []*current.Interface{{ 120 | Name: args.IfName, 121 | Sandbox: netns.Path(), 122 | }} 123 | 124 | if !netConf.DPDKMode { 125 | err = sm.SetupVF(netConf, args.IfName, netns) 126 | 127 | if err != nil { 128 | return fmt.Errorf("failed to set up pod interface %q from the device %q: %v", args.IfName, netConf.Master, err) 129 | } 130 | } 131 | 132 | result.Interfaces[0].Mac = config.GetMacAddressForResult(netConf) 133 | // check if we are able to find MTU for the virtual function 134 | if netConf.MTU != nil { 135 | result.Interfaces[0].Mtu = *netConf.MTU 136 | } 137 | 138 | doAnnounce := false 139 | 140 | // run the IPAM plugin 141 | if netConf.IPAM.Type != "" { 142 | var r types.Result 143 | r, err = ipam.ExecAdd(netConf.IPAM.Type, args.StdinData) 144 | if err != nil { 145 | return fmt.Errorf("failed to set up IPAM plugin type %q from the device %q: %v", netConf.IPAM.Type, netConf.Master, err) 146 | } 147 | 148 | defer func() { 149 | if err != nil { 150 | _ = ipam.ExecDel(netConf.IPAM.Type, args.StdinData) 151 | } 152 | }() 153 | 154 | // Convert the IPAM result into the current Result type 155 | var newResult *current.Result 156 | newResult, err = current.NewResultFromResult(r) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if len(newResult.IPs) == 0 { 162 | err = errors.New("IPAM plugin returned missing IP config") 163 | return err 164 | } 165 | 166 | newResult.Interfaces = result.Interfaces 167 | 168 | for _, ipc := range newResult.IPs { 169 | // All addresses apply to the container interface (move from host) 170 | ipc.Interface = current.Int(0) 171 | } 172 | 173 | if !netConf.DPDKMode { 174 | err = netns.Do(func(_ ns.NetNS) error { 175 | return ipam.ConfigureIface(args.IfName, newResult) 176 | }) 177 | if err != nil { 178 | return err 179 | } 180 | doAnnounce = true 181 | } 182 | result = newResult 183 | } 184 | 185 | // Cache NetConf for CmdDel 186 | logging.Debug("Cache NetConf for CmdDel", 187 | "func", "cmdAdd", 188 | "config.DefaultCNIDir", config.DefaultCNIDir, 189 | "netConf", netConf) 190 | if err = utils.SaveNetConf(args.ContainerID, config.DefaultCNIDir, args.IfName, netConf); err != nil { 191 | return fmt.Errorf("error saving NetConf %q", err) 192 | } 193 | 194 | // Mark the pci address as in use. 195 | logging.Debug("Mark the PCI address as in use", 196 | "func", "cmdAdd", 197 | "config.DefaultCNIDir", config.DefaultCNIDir, 198 | "netConf.DeviceID", netConf.DeviceID) 199 | allocator := utils.NewPCIAllocator(config.DefaultCNIDir) 200 | if err = allocator.SaveAllocatedPCI(netConf.DeviceID, args.Netns); err != nil { 201 | return fmt.Errorf("error saving the pci allocation for vf pci address %s: %v", netConf.DeviceID, err) 202 | } 203 | 204 | if doAnnounce { 205 | _ = netns.Do(func(_ ns.NetNS) error { 206 | /* After IPAM configuration is done, the following needs to handle the case of an IP address being reused by a different pods. 207 | * This is achieved by sending Gratuitous ARPs and/or Unsolicited Neighbor Advertisements unconditionally. 208 | * Although we set arp_notify and ndisc_notify unconditionally on the interface (please see EnableArpAndNdiscNotify()), the kernel 209 | * only sends GARPs/Unsolicited NA when the interface goes from down to up, or when the link-layer address changes on the interfaces. 210 | * These scenarios are perfectly valid and recommended to be enabled for optimal network performance. 211 | * However for our specific case, which the kernel is unaware of, is the reuse of IP addresses across pods where each pod has a different 212 | * link-layer address for it's SRIOV interface. The ARP/Neighbor cache residing in neighbors would be invalid if an IP address is reused. 213 | * In order to update the cache, the GARP/Unsolicited NA packets should be sent for performance reasons. Otherwise, the neighbors 214 | * may be sending packets with the incorrect link-layer address. Eventually, most network stacks would send ARPs and/or Neighbor 215 | * Solicitation packets when the connection is unreachable. This would correct the invalid cache; however this may take a significant 216 | * amount of time to complete. 217 | */ 218 | 219 | /* The interface might not yet have carrier. Wait for it for a short time. */ 220 | hasCarrier := utils.WaitForCarrier(args.IfName, 200*time.Millisecond) 221 | 222 | /* The error is ignored here because enabling this feature is only a performance enhancement. */ 223 | err := utils.AnnounceIPs(args.IfName, result.IPs) 224 | 225 | logging.Debug("announcing IPs", "hasCarrier", hasCarrier, "IPs", result.IPs, "announceError", err) 226 | return nil 227 | }) 228 | } 229 | 230 | return types.PrintResult(result, netConf.CNIVersion) 231 | } 232 | 233 | func CmdDel(args *skel.CmdArgs) error { 234 | if err := config.SetLogging(args.StdinData, args.ContainerID, args.Netns, args.IfName); err != nil { 235 | return err 236 | } 237 | logging.Debug("function called", 238 | "func", "cmdDel", 239 | "args.Path", args.Path, "args.StdinData", string(args.StdinData), "args.Args", args.Args) 240 | 241 | netConf, cRefPath, err := config.LoadConfFromCache(args) 242 | if err != nil { 243 | // If cmdDel() fails, cached netconf is cleaned up by 244 | // the followed defer call. However, subsequence calls 245 | // of cmdDel() from kubelet fail in a dead loop due to 246 | // cached netconf doesn't exist. 247 | // Return nil when LoadConfFromCache fails since the rest 248 | // of cmdDel() code relies on netconf as input argument 249 | // and there is no meaning to continue. 250 | logging.Error("Cannot load config file from cache", 251 | "func", "cmdDel", 252 | "err", err) 253 | return nil 254 | } 255 | 256 | allocator := utils.NewPCIAllocator(config.DefaultCNIDir) 257 | 258 | err = allocator.Lock(netConf.DeviceID) 259 | if err != nil { 260 | return fmt.Errorf("cmdDel() error obtaining lock for device [%s]: %w", netConf.DeviceID, err) 261 | } 262 | 263 | logging.Debug("Acquired device lock", 264 | "func", "cmdDel", 265 | "DeviceID", netConf.DeviceID) 266 | 267 | defer func() { 268 | if err == nil && cRefPath != "" { 269 | _ = utils.CleanCachedNetConf(cRefPath) 270 | } 271 | }() 272 | 273 | if netConf.IPAM.Type != "" { 274 | err = ipam.ExecDel(netConf.IPAM.Type, args.StdinData) 275 | if err != nil { 276 | return err 277 | } 278 | } 279 | 280 | // https://github.com/kubernetes/kubernetes/pull/35240 281 | if args.Netns == "" { 282 | return nil 283 | } 284 | 285 | // Verify VF ID existence. 286 | if _, err := utils.GetVfid(netConf.DeviceID, netConf.Master); err != nil { 287 | return fmt.Errorf("cmdDel() error obtaining VF ID: %q", err) 288 | } 289 | 290 | sm := sriov.NewSriovManager() 291 | 292 | logging.Debug("Reset VF configuration", 293 | "func", "cmdDel", 294 | "netConf.DeviceID", netConf.DeviceID) 295 | /* ResetVFConfig resets a VF administratively. We must run ResetVFConfig 296 | before ReleaseVF because some drivers will error out if we try to 297 | reset netdev VF with trust off. So, reset VF MAC address via PF first. 298 | */ 299 | if err := sm.ResetVFConfig(netConf); err != nil { 300 | return fmt.Errorf("cmdDel() error reseting VF: %q", err) 301 | } 302 | 303 | if !netConf.DPDKMode { 304 | netns, err := ns.GetNS(args.Netns) 305 | if err != nil { 306 | // according to: 307 | // https://github.com/kubernetes/kubernetes/issues/43014#issuecomment-287164444 308 | // if provided path does not exist (e.x. when node was restarted) 309 | // plugin should silently return with success after releasing 310 | // IPAM resources 311 | _, ok := err.(ns.NSPathNotExistErr) 312 | if ok { 313 | logging.Debug("Exiting as the network namespace does not exists anymore", 314 | "func", "cmdDel", 315 | "netConf.DeviceID", netConf.DeviceID, 316 | "args.Netns", args.Netns) 317 | return nil 318 | } 319 | 320 | return fmt.Errorf("failed to open netns %s: %q", netns, err) 321 | } 322 | defer netns.Close() 323 | 324 | logging.Debug("Release the VF", 325 | "func", "cmdDel", 326 | "netConf.DeviceID", netConf.DeviceID, 327 | "args.Netns", args.Netns, 328 | "args.IfName", args.IfName) 329 | if err := sm.ReleaseVF(netConf, args.IfName, netns); err != nil { 330 | return err 331 | } 332 | } 333 | 334 | // Mark the pci address as released 335 | logging.Debug("Mark the PCI address as released", 336 | "func", "cmdDel", 337 | "config.DefaultCNIDir", config.DefaultCNIDir, 338 | "netConf.DeviceID", netConf.DeviceID) 339 | if err = allocator.DeleteAllocatedPCI(netConf.DeviceID); err != nil { 340 | return fmt.Errorf("error cleaning the pci allocation for vf pci address %s: %v", netConf.DeviceID, err) 341 | } 342 | 343 | return nil 344 | } 345 | 346 | func CmdCheck(_ *skel.CmdArgs) error { 347 | return nil 348 | } 349 | -------------------------------------------------------------------------------- /pkg/utils/packet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "bytes" 21 | "encoding/binary" 22 | "errors" 23 | "fmt" 24 | "net" 25 | "syscall" 26 | "time" 27 | 28 | current "github.com/containernetworking/cni/pkg/types/100" 29 | "github.com/vishvananda/netlink" 30 | "golang.org/x/net/icmp" 31 | "golang.org/x/net/ipv6" 32 | "golang.org/x/sys/unix" 33 | ) 34 | 35 | var ( 36 | arpPacketName = "ARP" 37 | icmpV6PacketName = "ICMPv6" 38 | ) 39 | 40 | // htons converts an uint16 from host to network byte order. 41 | func htons(i uint16) uint16 { 42 | return (i<<8)&0xff00 | i>>8 43 | } 44 | 45 | // formatPacketFieldWriteError builds an error string for the cases when writing to a field of a packet fails. 46 | func formatPacketFieldWriteError(field, packetType string, writeErr error) error { 47 | return fmt.Errorf("failed to write the %s field in the %s packet: %v", field, packetType, writeErr) 48 | } 49 | 50 | // SendGratuitousArp sends a gratuitous ARP packet with the provided source IP over the provided interface. 51 | func SendGratuitousArp(srcIP net.IP, linkObj netlink.Link) error { 52 | /* As per RFC 5944 section 4.6, a gratuitous ARP packet can be sent by a node in order to spontaneously cause other nodes to update 53 | * an entry in their ARP cache. In the case of SRIOV-CNI, an address can be reused for different pods. Each pod could likely have a 54 | * different link-layer address in this scenario, which makes the ARP cache entries residing in the other nodes to be an invalid. 55 | * The gratuitous ARP packet should update the link-layer address accordingly for the invalid ARP cache. 56 | */ 57 | 58 | // Construct the ARP packet following RFC 5944 section 4.6. 59 | arpPacket := new(bytes.Buffer) 60 | if writeErr := binary.Write(arpPacket, binary.BigEndian, uint16(1)); writeErr != nil { // Hardware Type: 1 is Ethernet 61 | return formatPacketFieldWriteError("Hardware Type", arpPacketName, writeErr) 62 | } 63 | if writeErr := binary.Write(arpPacket, binary.BigEndian, uint16(syscall.ETH_P_IP)); writeErr != nil { // Protocol Type: 0x0800 is IPv4 64 | return formatPacketFieldWriteError("Protocol Type", arpPacketName, writeErr) 65 | } 66 | if writeErr := binary.Write(arpPacket, binary.BigEndian, uint8(6)); writeErr != nil { // Hardware address Length: 6 bytes for MAC address 67 | return formatPacketFieldWriteError("Hardware address Length", arpPacketName, writeErr) 68 | } 69 | if writeErr := binary.Write(arpPacket, binary.BigEndian, uint8(4)); writeErr != nil { // Protocol address length: 4 bytes for IPv4 address 70 | return formatPacketFieldWriteError("Protocol address length", arpPacketName, writeErr) 71 | } 72 | if writeErr := binary.Write(arpPacket, binary.BigEndian, uint16(1)); writeErr != nil { // Operation: 1 is request, 2 is response 73 | return formatPacketFieldWriteError("Operation", arpPacketName, writeErr) 74 | } 75 | if _, writeErr := arpPacket.Write(linkObj.Attrs().HardwareAddr); writeErr != nil { // Sender hardware address 76 | return formatPacketFieldWriteError("Sender hardware address", arpPacketName, writeErr) 77 | } 78 | if _, writeErr := arpPacket.Write(srcIP.To4()); writeErr != nil { // Sender protocol address 79 | return formatPacketFieldWriteError("Sender protocol address", arpPacketName, writeErr) 80 | } 81 | if _, writeErr := arpPacket.Write([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); writeErr != nil { // Target hardware address is the Broadcast MAC. 82 | return formatPacketFieldWriteError("Target hardware address", arpPacketName, writeErr) 83 | } 84 | if _, writeErr := arpPacket.Write(srcIP.To4()); writeErr != nil { // Target protocol address 85 | return formatPacketFieldWriteError("Target protocol address", arpPacketName, writeErr) 86 | } 87 | 88 | sockAddr := syscall.SockaddrLinklayer{ 89 | Protocol: htons(syscall.ETH_P_ARP), // Ethertype of ARP (0x0806) 90 | Ifindex: linkObj.Attrs().Index, // Interface Index 91 | Hatype: 1, // Hardware Type: 1 is Ethernet 92 | Pkttype: 0, // Packet Type. 93 | Halen: 6, // Hardware address Length: 6 bytes for MAC address 94 | Addr: [8]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // Address is the broadcast MAC address. 95 | } 96 | 97 | // Create a socket such that the Ethernet header would constructed by the OS. The arpPacket only contains the ARP payload. 98 | soc, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_DGRAM, int(htons(syscall.ETH_P_ARP))) 99 | if err != nil { 100 | return fmt.Errorf("failed to create AF_PACKET datagram socket: %v", err) 101 | } 102 | defer syscall.Close(soc) 103 | 104 | if err := syscall.Sendto(soc, arpPacket.Bytes(), 0, &sockAddr); err != nil { 105 | return fmt.Errorf("failed to send Gratuitous ARP for IPv4 %s on Interface %s: %v", srcIP.String(), linkObj.Attrs().Name, err) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // SendUnsolicitedNeighborAdvertisement sends an unsolicited neighbor advertisement packet with the provided source IP over the provided interface. 112 | func SendUnsolicitedNeighborAdvertisement(srcIP net.IP, linkObj netlink.Link) error { 113 | /* As per RFC 4861, a link-layer address change can multicast a few unsolicited neighbor advertisements to all nodes to quickly 114 | * update the cached link-layer addresses that have become invalid. In the case of SRIOV-CNI, an address can be reused for 115 | * different pods. Each pod could likely have a different link-layer address in this scenario, which makes the Neighbor Cache 116 | * entries residing in the neighbors to be an invalid. The unsolicited neighbor advertisement should update the link-layer address 117 | * accordingly for the IPv6 entry. 118 | * However if any of these conditions are true: 119 | * - The IPv6 address was not reused for the new pod. 120 | * - No prior established communication with the neighbor. 121 | * Then the neighbor receiving this unsolicited neighbor advertisement would be silently discard. This behavior is described 122 | * in RFC 4861 section 7.2.5. This is acceptable behavior since the purpose of sending an unsolicited neighbor advertisement 123 | * is not to create a new entry but rather update already existing invalid entries. 124 | */ 125 | 126 | // Construct the ICMPv6 Neighbor Advertisement packet following RFC 4861. 127 | payload := new(bytes.Buffer) 128 | // ICMPv6 Flags: As per RFC 4861, the solicited flag must not be set and the override flag should be set (to 129 | // override existing cache entry) for unsolicited advertisements. 130 | if writeErr := binary.Write(payload, binary.BigEndian, uint32(0x20000000)); writeErr != nil { 131 | return formatPacketFieldWriteError("Flags", icmpV6PacketName, writeErr) 132 | } 133 | if _, writeErr := payload.Write(srcIP.To16()); writeErr != nil { // ICMPv6 Target IPv6 Address. 134 | return formatPacketFieldWriteError("Target IPv6 Address", icmpV6PacketName, writeErr) 135 | } 136 | if writeErr := binary.Write(payload, binary.BigEndian, uint8(2)); writeErr != nil { // ICMPv6 Option Type: 2 is target link-layer address. 137 | return formatPacketFieldWriteError("Option Type", icmpV6PacketName, writeErr) 138 | } 139 | if writeErr := binary.Write(payload, binary.BigEndian, uint8(1)); writeErr != nil { // ICMPv6 Option Length. Units of 8 bytes. 140 | return formatPacketFieldWriteError("Option Length", icmpV6PacketName, writeErr) 141 | } 142 | if _, writeErr := payload.Write(linkObj.Attrs().HardwareAddr); writeErr != nil { // ICMPv6 Option Link-layer Address. 143 | return formatPacketFieldWriteError("Option Link-layer Address", icmpV6PacketName, writeErr) 144 | } 145 | 146 | icmpv6Msg := icmp.Message{ 147 | Type: ipv6.ICMPTypeNeighborAdvertisement, // ICMPv6 type is neighbor advertisement. 148 | Code: 0, // ICMPv6 Code: As per RFC 4861 section 7.1.2, the code is always 0. 149 | Checksum: 0, // Checksum is calculated later. 150 | Body: &icmp.RawBody{ 151 | Data: payload.Bytes(), 152 | }, 153 | } 154 | 155 | // Get the byte array of the ICMPv6 Message. 156 | icmpv6Bytes, err := icmpv6Msg.Marshal(nil) 157 | if err != nil { 158 | return fmt.Errorf("failed to Marshal ICMPv6 Message: %v", err) 159 | } 160 | 161 | // Create a socket such that the Ethernet header and IPv6 header would constructed by the OS. 162 | soc, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_ICMPV6) 163 | if err != nil { 164 | return fmt.Errorf("failed to create AF_INET6 raw socket: %v", err) 165 | } 166 | defer syscall.Close(soc) 167 | 168 | // As per RFC 4861 section 7.1.2, the IPv6 hop limit is always 255. 169 | if err := syscall.SetsockoptInt(soc, syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_HOPS, 255); err != nil { 170 | return fmt.Errorf("failed to set IPv6 multicast hops to 255: %v", err) 171 | } 172 | 173 | // Set the destination IPv6 address to the IPv6 link-local all nodes multicast address (ff02::1). 174 | var r [16]byte 175 | copy(r[:], net.IPv6linklocalallnodes.To16()) 176 | sockAddr := syscall.SockaddrInet6{Addr: r} 177 | if err := syscall.Sendto(soc, icmpv6Bytes, 0, &sockAddr); err != nil { 178 | return fmt.Errorf("failed to send Unsolicited Neighbor Advertisement for IPv6 %s on Interface %s: %v", srcIP.String(), linkObj.Attrs().Name, err) 179 | } 180 | 181 | return nil 182 | } 183 | 184 | // AnnounceIPs sends IPv4 GARP and IPv6 Unsolicited NA for the addresses on the 185 | // interfaces. If ifName is not found or has no MAC address, an error is 186 | // returned. If sending of announcements fail, the returned error is a combined 187 | // errors.Join() of each failed announcement. Despite such errors, remaining 188 | // addresses are still attempted to be announced. 189 | func AnnounceIPs(ifName string, ipConfigs []*current.IPConfig) error { 190 | // Retrieve the interface name in the container. 191 | linkObj, err := netLinkLib.LinkByName(ifName) 192 | if err != nil { 193 | return fmt.Errorf("failed to get netlink device with name %q: %v", ifName, err) 194 | } 195 | if !IsValidMACAddress(linkObj.Attrs().HardwareAddr) { 196 | return fmt.Errorf("invalid Ethernet MAC address: %q", linkObj.Attrs().HardwareAddr) 197 | } 198 | 199 | var errResult error 200 | 201 | // For all the IP addresses assigned by IPAM, we will send either a GARP (IPv4) or Unsolicited NA (IPv6). 202 | for _, ipc := range ipConfigs { 203 | if IsIPv4(ipc.Address.IP) { 204 | err := SendGratuitousArp(ipc.Address.IP, linkObj) 205 | if err != nil { 206 | errResult = errors.Join(errResult, fmt.Errorf("failed to send GARP message for ip %s on interface %q: %v", ipc.Address.IP.String(), ifName, err)) 207 | } 208 | } else if IsIPv6(ipc.Address.IP) { 209 | /* As per RFC 4861, sending unsolicited neighbor advertisements should be considered as a performance 210 | * optimization. It does not reliably update caches in all nodes. The Neighbor Unreachability Detection 211 | * algorithm is more reliable although it may take slightly longer to update. 212 | */ 213 | err := SendUnsolicitedNeighborAdvertisement(ipc.Address.IP, linkObj) 214 | if err != nil { 215 | errResult = errors.Join(errResult, fmt.Errorf("failed to send NA message for ip %s on interface %q: %v", ipc.Address.IP.String(), ifName, err)) 216 | } 217 | } 218 | } 219 | return errResult 220 | } 221 | 222 | // Blocking wait for interface ifName to have carrier (!NO_CARRIER flag). 223 | func WaitForCarrier(ifName string, waitTime time.Duration) bool { 224 | var nextSleepDuration time.Duration 225 | 226 | start := time.Now() 227 | 228 | for nextSleepDuration == 0 || time.Since(start) < waitTime { 229 | if nextSleepDuration == 0 { 230 | nextSleepDuration = 2 * time.Millisecond 231 | } else { 232 | time.Sleep(nextSleepDuration) 233 | /* Grow wait time exponentionally (factor 1.5). */ 234 | nextSleepDuration += nextSleepDuration / 2 235 | } 236 | 237 | linkObj, err := netLinkLib.LinkByName(ifName) 238 | if err != nil { 239 | return false 240 | } 241 | 242 | /* Wait for carrier, i.e. IFF_UP|IFF_RUNNING. Note that there is also 243 | * IFF_LOWER_UP, but we follow iproute2 ([1]). 244 | * 245 | * [1] https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ipaddress.c?id=f9601b10c21145f76c3d46c163bac39515ed2061#n86 246 | */ 247 | if linkObj.Attrs().RawFlags&(unix.IFF_UP|unix.IFF_RUNNING) == (unix.IFF_UP | unix.IFF_RUNNING) { 248 | return true 249 | } 250 | } 251 | 252 | return false 253 | } 254 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 sriov-cni authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | package utils 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "net" 24 | "os" 25 | "path/filepath" 26 | "strconv" 27 | "strings" 28 | "time" 29 | 30 | sriovtypes "github.com/k8snetworkplumbingwg/sriov-cni/pkg/types" 31 | ) 32 | 33 | var ( 34 | sriovConfigured = "/sriov_numvfs" 35 | // NetDirectory sysfs net directory 36 | NetDirectory = "/sys/class/net" 37 | // SysBusPci is sysfs pci device directory 38 | SysBusPci = "/sys/bus/pci/devices" 39 | // SysV4ArpNotify is the sysfs IPv4 ARP Notify directory 40 | SysV4ArpNotify = "/proc/sys/net/ipv4/conf/" 41 | // SysV6NdiscNotify is the sysfs IPv6 Neighbor Discovery Notify directory 42 | SysV6NdiscNotify = "/proc/sys/net/ipv6/conf/" 43 | // UserspaceDrivers is a list of driver names that don't have netlink representation for their devices 44 | UserspaceDrivers = []string{"vfio-pci", "uio_pci_generic", "igb_uio"} 45 | ) 46 | 47 | // EnableArpAndNdiscNotify enables IPv4 arp_notify and IPv6 ndisc_notify for netdev 48 | func EnableArpAndNdiscNotify(ifName string) error { 49 | /* For arp_notify, when a value of "1" is set then a Gratuitous ARP request will be sent 50 | * when the network device is brought up or if the link-layer address changes. 51 | * For ndsic_notify, when a value of "1" is set then a Unsolicited Neighbor Advertisement 52 | * will be sent when the network device is brought up or if the link-layer address changes. 53 | * Both of these being enabled would be useful in the case when an application reenables 54 | * an interface or if the MAC address configuration is changed. The kernel is responsible 55 | * for sending of these packets when the conditions are met. 56 | */ 57 | v4ArpNotifyPath := filepath.Join(SysV4ArpNotify, ifName, "arp_notify") 58 | err := os.WriteFile(v4ArpNotifyPath, []byte("1"), os.ModeAppend) 59 | if err != nil { 60 | return fmt.Errorf("failed to write arp_notify=1 for interface %s: %v", ifName, err) 61 | } 62 | v6NdiscNotifyPath := filepath.Join(SysV6NdiscNotify, ifName, "ndisc_notify") 63 | err = os.WriteFile(v6NdiscNotifyPath, []byte("1"), os.ModeAppend) 64 | if err != nil { 65 | return fmt.Errorf("failed to write ndisc_notify=1 for interface %s: %v", ifName, err) 66 | } 67 | return nil 68 | } 69 | 70 | // EnableOptimisticDad enables IPv6 /proc/sys/net/ipv6/conf/$ifName/optimistic_dad 71 | func EnableOptimisticDad(ifName string) error { 72 | path := filepath.Join(SysV6NdiscNotify, ifName, "optimistic_dad") 73 | err := os.WriteFile(path, []byte("1"), os.ModeAppend) 74 | if err != nil { 75 | return fmt.Errorf("failed to write optimistic_dad=1 for interface %s: %v", ifName, err) 76 | } 77 | return nil 78 | } 79 | 80 | // GetSriovNumVfs takes in a PF name(ifName) as string and returns number of VF configured as int 81 | func GetSriovNumVfs(ifName string) (int, error) { 82 | var vfTotal int 83 | 84 | sriovFile := filepath.Join(NetDirectory, ifName, "device", sriovConfigured) 85 | if _, err := os.Lstat(sriovFile); err != nil { 86 | return vfTotal, fmt.Errorf("failed to open the sriov_numfs of device %q: %v", ifName, err) 87 | } 88 | 89 | data, err := os.ReadFile(sriovFile) 90 | if err != nil { 91 | return vfTotal, fmt.Errorf("failed to read the sriov_numfs of device %q: %v", ifName, err) 92 | } 93 | 94 | if len(data) == 0 { 95 | return vfTotal, fmt.Errorf("no data in the file %q", sriovFile) 96 | } 97 | 98 | sriovNumfs := strings.TrimSpace(string(data)) 99 | vfTotal, err = strconv.Atoi(sriovNumfs) 100 | if err != nil { 101 | return vfTotal, fmt.Errorf("failed to convert sriov_numfs(byte value) to int of device %q: %v", ifName, err) 102 | } 103 | 104 | return vfTotal, nil 105 | } 106 | 107 | // GetVfid takes in VF's PCI address(addr) and pfName as string and returns VF's ID as int 108 | func GetVfid(addr, pfName string) (int, error) { 109 | var id int 110 | vfTotal, err := GetSriovNumVfs(pfName) 111 | if err != nil { 112 | return id, err 113 | } 114 | for vf := 0; vf < vfTotal; vf++ { 115 | vfDir := filepath.Join(NetDirectory, pfName, "device", fmt.Sprintf("virtfn%d", vf)) 116 | _, err := os.Lstat(vfDir) 117 | if err != nil { 118 | continue 119 | } 120 | pciinfo, err := os.Readlink(vfDir) 121 | if err != nil { 122 | continue 123 | } 124 | pciaddr := filepath.Base(pciinfo) 125 | if pciaddr == addr { 126 | return vf, nil 127 | } 128 | } 129 | return id, fmt.Errorf("unable to get VF ID with PF: %s and VF pci address %v", pfName, addr) 130 | } 131 | 132 | // GetPfName returns PF net device name of a given VF pci address 133 | func GetPfName(vf string) (string, error) { 134 | pfSymLink := filepath.Join(SysBusPci, vf, "physfn", "net") 135 | _, err := os.Lstat(pfSymLink) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | files, err := os.ReadDir(pfSymLink) 141 | if err != nil { 142 | return "", err 143 | } 144 | 145 | if len(files) < 1 { 146 | return "", fmt.Errorf("PF network device not found") 147 | } 148 | 149 | return strings.TrimSpace(files[0].Name()), nil 150 | } 151 | 152 | // GetPciAddress takes in a interface(ifName) and VF id and returns its pci addr as string 153 | func GetPciAddress(ifName string, vf int) (string, error) { 154 | var pciaddr string 155 | vfDir := filepath.Join(NetDirectory, ifName, "device", fmt.Sprintf("virtfn%d", vf)) 156 | dirInfo, err := os.Lstat(vfDir) 157 | if err != nil { 158 | return pciaddr, fmt.Errorf("can't get the symbolic link of virtfn%d dir of the device %q: %v", vf, ifName, err) 159 | } 160 | 161 | if (dirInfo.Mode() & os.ModeSymlink) == 0 { 162 | return pciaddr, fmt.Errorf("no symbolic link for the virtfn%d dir of the device %q", vf, ifName) 163 | } 164 | 165 | pciinfo, err := os.Readlink(vfDir) 166 | if err != nil { 167 | return pciaddr, fmt.Errorf("can't read the symbolic link of virtfn%d dir of the device %q: %v", vf, ifName, err) 168 | } 169 | 170 | pciaddr = filepath.Base(pciinfo) 171 | return pciaddr, nil 172 | } 173 | 174 | // GetSharedPF takes in VF name(ifName) as string and returns the other VF name that shares same PCI address as string 175 | func GetSharedPF(ifName string) (string, error) { 176 | pfName := "" 177 | pfDir := filepath.Join(NetDirectory, ifName) 178 | dirInfo, err := os.Lstat(pfDir) 179 | if err != nil { 180 | return pfName, fmt.Errorf("can't get the symbolic link of the device %q: %v", ifName, err) 181 | } 182 | 183 | if (dirInfo.Mode() & os.ModeSymlink) == 0 { 184 | return pfName, fmt.Errorf("no symbolic link for dir of the device %q", ifName) 185 | } 186 | 187 | fullpath, _ := filepath.EvalSymlinks(pfDir) 188 | parentDir := fullpath[:len(fullpath)-len(ifName)] 189 | dirList, _ := os.ReadDir(parentDir) 190 | 191 | for _, file := range dirList { 192 | if file.Name() != ifName { 193 | pfName = file.Name() 194 | return pfName, nil 195 | } 196 | } 197 | 198 | return pfName, fmt.Errorf("shared PF not found") 199 | } 200 | 201 | // GetVFLinkName returns VF's network interface name given it's PCI addr 202 | func GetVFLinkName(pciAddr string) (string, error) { 203 | var names []string 204 | vfDir := filepath.Join(SysBusPci, pciAddr, "net") 205 | if _, err := os.Lstat(vfDir); err != nil { 206 | return "", err 207 | } 208 | 209 | fInfos, err := os.ReadDir(vfDir) 210 | if err != nil { 211 | return "", fmt.Errorf("failed to read net dir of the device %s: %v", pciAddr, err) 212 | } 213 | 214 | if len(fInfos) == 0 { 215 | return "", fmt.Errorf("VF device %s sysfs path (%s) has no entries", pciAddr, vfDir) 216 | } 217 | 218 | names = make([]string, len(fInfos)) 219 | for idx, f := range fInfos { 220 | names[idx] = f.Name() 221 | } 222 | 223 | if len(names) < 1 { 224 | return "", fmt.Errorf("VF device %s has no entries", pciAddr) 225 | } 226 | return names[0], nil 227 | } 228 | 229 | // GetVFLinkNamesFromVFID returns VF's network interface name given it's PF name as string and VF id as int 230 | func GetVFLinkNamesFromVFID(pfName string, vfID int) ([]string, error) { 231 | vfDir := filepath.Join(NetDirectory, pfName, "device", fmt.Sprintf("virtfn%d", vfID), "net") 232 | if _, err := os.Lstat(vfDir); err != nil { 233 | return nil, err 234 | } 235 | 236 | fInfos, err := os.ReadDir(vfDir) 237 | if err != nil { 238 | return nil, fmt.Errorf("failed to read the virtfn%d dir of the device %q: %v", vfID, pfName, err) 239 | } 240 | 241 | names := make([]string, 0) 242 | for _, f := range fInfos { 243 | names = append(names, f.Name()) 244 | } 245 | 246 | return names, nil 247 | } 248 | 249 | // HasDpdkDriver checks if a device is attached to dpdk supported driver 250 | func HasDpdkDriver(pciAddr string) (bool, error) { 251 | driverLink := filepath.Join(SysBusPci, pciAddr, "driver") 252 | driverPath, err := filepath.EvalSymlinks(driverLink) 253 | if err != nil { 254 | return false, err 255 | } 256 | driverStat, err := os.Stat(driverPath) 257 | if err != nil { 258 | return false, err 259 | } 260 | driverName := driverStat.Name() 261 | for _, drv := range UserspaceDrivers { 262 | if driverName == drv { 263 | return true, nil 264 | } 265 | } 266 | return false, nil 267 | } 268 | 269 | // SaveNetConf takes in container ID, data dir and Pod interface name as string and a json encoded struct Conf 270 | // and save this Conf in data dir 271 | func SaveNetConf(cid, dataDir, podIfName string, netConf *sriovtypes.NetConf) error { 272 | netConfBytes, err := json.Marshal(netConf) 273 | if err != nil { 274 | return fmt.Errorf("error serializing delegate netConf: %v", err) 275 | } 276 | 277 | s := []string{cid, podIfName} 278 | cRef := strings.Join(s, "-") 279 | 280 | // save the rendered netconf for cmdDel 281 | return saveScratchNetConf(cRef, dataDir, netConfBytes) 282 | } 283 | 284 | func saveScratchNetConf(containerID, dataDir string, netconf []byte) error { 285 | if err := os.MkdirAll(dataDir, 0700); err != nil { 286 | return fmt.Errorf("failed to create the sriov data directory(%q): %v", dataDir, err) 287 | } 288 | 289 | path := filepath.Join(dataDir, containerID) 290 | 291 | err := os.WriteFile(path, netconf, 0600) 292 | if err != nil { 293 | return fmt.Errorf("failed to write container data in the path(%q): %v", path, err) 294 | } 295 | 296 | return err 297 | } 298 | 299 | // ReadScratchNetConf takes in container ID, Pod interface name and data dir as string and returns a pointer to Conf 300 | func ReadScratchNetConf(cRefPath string) ([]byte, error) { 301 | data, err := os.ReadFile(cRefPath) 302 | if err != nil { 303 | return nil, fmt.Errorf("failed to read container data in the path(%q): %v", cRefPath, err) 304 | } 305 | 306 | return data, err 307 | } 308 | 309 | // CleanCachedNetConf removed cached NetConf from disk 310 | func CleanCachedNetConf(cRefPath string) error { 311 | if err := os.Remove(cRefPath); err != nil { 312 | return fmt.Errorf("error removing NetConf file %s: %v", cRefPath, err) 313 | } 314 | return nil 315 | } 316 | 317 | // SetVFEffectiveMAC will try to set the mac address on a specific VF interface 318 | // 319 | // the function will also validate that the mac address was configured as expect 320 | // it will return an error if it didn't manage to configure the vf mac address 321 | // or the mac is not equal to the expect one 322 | // retries 20 times and wait 100 milliseconds 323 | // 324 | // Some NIC drivers (i.e. i40e/iavf) set VF MAC address asynchronously 325 | // via PF. This means that while the PF could already show the VF with 326 | // the desired MAC address, the netdev VF may still have the original 327 | // one. If in this window we issue a netdev VF MAC address set, the driver 328 | // will return an error and the pod will fail to create. 329 | // Other NICs (Mellanox) require explicit netdev VF MAC address so we 330 | // cannot skip this part. 331 | // Retry up to 5 times; wait 200 milliseconds between retries 332 | func SetVFEffectiveMAC(netLinkManager NetlinkManager, netDeviceName, macAddress string) error { 333 | hwaddr, err := net.ParseMAC(macAddress) 334 | if err != nil { 335 | return fmt.Errorf("failed to parse MAC address %s: %v", macAddress, err) 336 | } 337 | 338 | orgLinkObj, err := netLinkManager.LinkByName(netDeviceName) 339 | if err != nil { 340 | return err 341 | } 342 | 343 | return Retry(20, 100*time.Millisecond, func() error { 344 | if err := netLinkManager.LinkSetHardwareAddr(orgLinkObj, hwaddr); err != nil { 345 | return err 346 | } 347 | 348 | linkObj, err := netLinkManager.LinkByName(netDeviceName) 349 | if err != nil { 350 | return fmt.Errorf("failed to get netlink device with name %s: %q", orgLinkObj.Attrs().Name, err) 351 | } 352 | if linkObj.Attrs().HardwareAddr.String() != macAddress { 353 | return fmt.Errorf("effective mac address is different from requested one") 354 | } 355 | 356 | return nil 357 | }) 358 | } 359 | 360 | // SetVFHardwareMAC will try to set the hardware mac address on a specific VF ID under a requested PF 361 | 362 | // the function will also validate that the mac address was configured as expect 363 | // it will return an error if it didn't manage to configure the vf mac address 364 | // or the mac is not equal to the expect one 365 | // retries 20 times and wait 100 milliseconds 366 | func SetVFHardwareMAC(netLinkManager NetlinkManager, pfDevice string, vfID int, macAddress string) error { 367 | hwaddr, err := net.ParseMAC(macAddress) 368 | if err != nil { 369 | return fmt.Errorf("failed to parse MAC address %s: %v", macAddress, err) 370 | } 371 | 372 | orgLinkObj, err := netLinkManager.LinkByName(pfDevice) 373 | if err != nil { 374 | return err 375 | } 376 | 377 | return Retry(20, 100*time.Millisecond, func() error { 378 | if err := netLinkManager.LinkSetVfHardwareAddr(orgLinkObj, vfID, hwaddr); err != nil { 379 | return err 380 | } 381 | 382 | linkObj, err := netLinkManager.LinkByName(pfDevice) 383 | if err != nil { 384 | return fmt.Errorf("failed to get netlink device with name %s: %q", orgLinkObj.Attrs().Name, err) 385 | } 386 | if linkObj.Attrs().Vfs[vfID].Mac.String() != macAddress { 387 | return fmt.Errorf("hardware mac address is different from requested one") 388 | } 389 | 390 | return nil 391 | }) 392 | } 393 | 394 | // IsValidMACAddress checks if net.HardwareAddr is a valid MAC address. 395 | func IsValidMACAddress(addr net.HardwareAddr) bool { 396 | invalidMACAddresses := [][]byte{ 397 | {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 398 | {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, 399 | } 400 | valid := false 401 | if len(addr) == 6 { 402 | valid = true 403 | for _, invalidMACAddress := range invalidMACAddresses { 404 | if bytes.Equal(addr, invalidMACAddress) { 405 | valid = false 406 | break 407 | } 408 | } 409 | } 410 | return valid 411 | } 412 | 413 | // IsIPv4 checks if a net.IP is an IPv4 address. 414 | func IsIPv4(ip net.IP) bool { 415 | return ip.To4() != nil 416 | } 417 | 418 | // IsIPv6 checks if a net.IP is an IPv6 address. 419 | func IsIPv6(ip net.IP) bool { 420 | return ip.To4() == nil && ip.To16() != nil 421 | } 422 | 423 | // Retry retries a given function until no return error; times out after retries*sleep 424 | func Retry(retries int, sleep time.Duration, f func() error) error { 425 | err := error(nil) 426 | for retry := 0; retry < retries; retry++ { 427 | err = f() 428 | if err == nil { 429 | return nil 430 | } 431 | time.Sleep(sleep) 432 | } 433 | return err 434 | } 435 | --------------------------------------------------------------------------------