Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/provisioning/controllers/util/dms/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ func CreateHostAgentPod(ctx context.Context, client client.Client, node *corev1.
Name: "run-udev",
MountPath: "/run/udev",
},
{
Name: "etc-udev-rules",
MountPath: "/etc/udev/rules.d",
},
{
Name: "systemd-network",
MountPath: "/usr/lib/systemd/network",
Expand Down Expand Up @@ -307,6 +311,15 @@ func CreateHostAgentPod(ctx context.Context, client client.Client, node *corev1.
},
},
},
{
Name: "etc-udev-rules",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/etc/udev/rules.d",
Type: ptr.To(corev1.HostPathDirectoryOrCreate),
},
},
},
{
Name: "systemd-network",
VolumeSource: corev1.VolumeSource{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ func (nm *NetworkManager) processNetworkRequest(nr NetworkRequest) error {
return nil
}
operations := []networkOperation{
{
name: "DisableNMForVFs",
f: func(nr NetworkRequest) error {
return nm.netBackend.EnsureVFsUnmanaged()
},
},
{
name: "CreateP0VF",
f: func(nr NetworkRequest) error {
Expand Down
5 changes: 5 additions & 0 deletions internal/provisioning/hostagent/util/netconfig/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ type Backend interface {

// IsDHCPConfigured checks if DHCP is enabled for an interface.
IsDHCPConfigured(interfaceName string) (bool, error)

// EnsureVFsUnmanaged ensures that VF interfaces will not be managed by the
// network configuration backend. For NetworkManager this writes a udev rule;
// other backends may no-op.
EnsureVFsUnmanaged() error
}

// ConfigureNetwork orchestrates PF interface and bridge MTU configuration
Expand Down
4 changes: 4 additions & 0 deletions internal/provisioning/hostagent/util/netconfig/nm_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func (n *NetworkManagerBackend) ResetPendingChanges() {
n.modifiedConnPaths = nil
}

func (n *NetworkManagerBackend) EnsureVFsUnmanaged() error {
return ensureNMUnmanagedUdevRule()
}

// ConfigurePFInterfaces configures physical function network interfaces via NM D-Bus.
func (n *NetworkManagerBackend) ConfigurePFInterfaces(pciAddress string, portConfigs []hostutil.PortConfig) (bool, error) {
needsApply := false
Expand Down
92 changes: 92 additions & 0 deletions internal/provisioning/hostagent/util/netconfig/nm_udev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
Copyright 2026 NVIDIA

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package netconfig

import (
"fmt"
"os"
"os/exec"
"path/filepath"

"k8s.io/klog/v2"
)

const (
nmUnmanagedRulesContent = `ACTION=="add|change|move", ATTRS{device}=="0x101e", ENV{NM_UNMANAGED}="1"
`
)

// nmUnmanagedRulesPath is the file path for the udev rule. Variable for testability.
var nmUnmanagedRulesPath = "/etc/udev/rules.d/10-nm-unmanaged.rules"

// udevRunner abstracts command execution for testability.
var udevRunner = func(name string, args ...string) ([]byte, error) {
return exec.Command(name, args...).CombinedOutput()
}

// ensureNMUnmanagedUdevRule writes a udev rule that prevents NetworkManager
// from managing VF interfaces (PCI device ID 0x101e) and reloads/triggers
// udev to apply the rule to both new and already-existing devices.
func ensureNMUnmanagedUdevRule() error {
written, err := writeUdevRuleFile()
if err != nil {
return fmt.Errorf("failed to write udev rule file: %w", err)
}
if !written {
return nil
}

if err := reloadAndTriggerUdev(); err != nil {
return fmt.Errorf("failed to reload/trigger udev rules: %w", err)
}

return nil
}

func writeUdevRuleFile() (bool, error) {
dir := filepath.Dir(nmUnmanagedRulesPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return false, fmt.Errorf("failed to create directory %s: %w", dir, err)
}

existing, err := os.ReadFile(nmUnmanagedRulesPath)
if err == nil && string(existing) == nmUnmanagedRulesContent {
klog.V(3).Infof("Udev rule %s already up-to-date", nmUnmanagedRulesPath)
return false, nil
}

if err := os.WriteFile(nmUnmanagedRulesPath, []byte(nmUnmanagedRulesContent), 0644); err != nil {
return false, fmt.Errorf("failed to write file %s: %w", nmUnmanagedRulesPath, err)
}
klog.Infof("Wrote udev rule to disable NM management of VFs: %s", nmUnmanagedRulesPath)
return true, nil
}

func reloadAndTriggerUdev() error {
output, err := udevRunner("udevadm", "control", "--reload-rules")
if err != nil {
return fmt.Errorf("udevadm control --reload-rules failed: %w, output: %s", err, string(output))
}

output, err = udevRunner("udevadm", "trigger", "--subsystem-match=net")
if err != nil {
return fmt.Errorf("udevadm trigger --subsystem-match=net failed: %w, output: %s", err, string(output))
}

klog.V(3).Infof("Reloaded udev rules and triggered net subsystem")
return nil
}
148 changes: 148 additions & 0 deletions internal/provisioning/hostagent/util/netconfig/nm_udev_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
Copyright 2026 NVIDIA

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package netconfig

import (
"fmt"
"os"
"path/filepath"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("ensureNMUnmanagedUdevRule", func() {
var (
origPath string
origRunner func(string, ...string) ([]byte, error)
tempDir string
commands [][]string
)

BeforeEach(func() {
var err error
tempDir, err = os.MkdirTemp("", "udev-test-*")
Expect(err).NotTo(HaveOccurred())

origPath = nmUnmanagedRulesPath
origRunner = udevRunner

commands = nil
udevRunner = func(name string, args ...string) ([]byte, error) {
commands = append(commands, append([]string{name}, args...))
return nil, nil
}
})

AfterEach(func() {
nmUnmanagedRulesPath = origPath
udevRunner = origRunner
os.RemoveAll(tempDir)
})

setRulesPath := func() string {
p := filepath.Join(tempDir, "10-nm-unmanaged.rules")
nmUnmanagedRulesPath = p
return p
}

It("should write the udev rule file and reload rules", func() {
rulesFile := setRulesPath()

err := ensureNMUnmanagedUdevRule()
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(rulesFile)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal(nmUnmanagedRulesContent))

Expect(commands).To(HaveLen(2))
Expect(commands[0]).To(Equal([]string{"udevadm", "control", "--reload-rules"}))
Expect(commands[1]).To(Equal([]string{"udevadm", "trigger", "--subsystem-match=net"}))
})

It("should be idempotent - skip reload if content matches", func() {
rulesFile := setRulesPath()

err := os.MkdirAll(filepath.Dir(rulesFile), 0755)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(rulesFile, []byte(nmUnmanagedRulesContent), 0644)
Expect(err).NotTo(HaveOccurred())

err = ensureNMUnmanagedUdevRule()
Expect(err).NotTo(HaveOccurred())

Expect(commands).To(BeEmpty())
})

It("should overwrite if content differs", func() {
rulesFile := setRulesPath()

err := os.MkdirAll(filepath.Dir(rulesFile), 0755)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(rulesFile, []byte("old content"), 0644)
Expect(err).NotTo(HaveOccurred())

err = ensureNMUnmanagedUdevRule()
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(rulesFile)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal(nmUnmanagedRulesContent))
})

It("should create parent directories if they don't exist", func() {
nmUnmanagedRulesPath = filepath.Join(tempDir, "subdir", "rules.d", "10-nm-unmanaged.rules")

err := ensureNMUnmanagedUdevRule()
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(nmUnmanagedRulesPath)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal(nmUnmanagedRulesContent))
})

It("should return error if udevadm reload fails", func() {
setRulesPath()

udevRunner = func(name string, args ...string) ([]byte, error) {
if len(args) > 0 && args[0] == "control" {
return []byte("reload failed"), fmt.Errorf("exit status 1")
}
return nil, nil
}

err := ensureNMUnmanagedUdevRule()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("udevadm control --reload-rules failed"))
})

It("should return error if udevadm trigger fails", func() {
setRulesPath()

udevRunner = func(name string, args ...string) ([]byte, error) {
if len(args) > 0 && args[0] == "trigger" {
return []byte("trigger failed"), fmt.Errorf("exit status 1")
}
return nil, nil
}

err := ensureNMUnmanagedUdevRule()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("udevadm trigger --subsystem-match=net failed"))
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ func (s *SystemdNetworkdBackend) ApplyConfiguration() error {
func (s *SystemdNetworkdBackend) IsDHCPConfigured(interfaceName string) (bool, error) {
return hostutil.IsDHCPConfigured(interfaceName)
}

func (s *SystemdNetworkdBackend) EnsureVFsUnmanaged() error {
return nil
}