Skip to content

Commit d9e2eea

Browse files
committed
feat: Go implement UDEV
Signed-off-by: Fred Rolland <[email protected]>
1 parent cd40b4c commit d9e2eea

File tree

3 files changed

+333
-4
lines changed

3 files changed

+333
-4
lines changed

entrypoint/internal/utils/udev/udev.go

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package udev
1818

1919
import (
2020
"context"
21+
"os"
2122

2223
"github.com/go-logr/logr"
2324

@@ -50,15 +51,76 @@ type udev struct {
5051

5152
// CreateRules is the default implementation of the udev.Interface.
5253
func (u *udev) CreateRules(ctx context.Context) error {
53-
_ = logr.FromContextOrDiscard(ctx)
54-
// TODO add implementation
54+
log := logr.FromContextOrDiscard(ctx)
55+
log.Info("create udev rules")
56+
57+
// Create the udev rules content
58+
// This creates rules to rename physical interfaces by removing the "np[n]" suffix
59+
// Example: enp8s0f0np0 -> enp8s0f0, enp8s0f0np1v12 -> enp8s0f0v12
60+
udevRulesContent := `ACTION!="add", GOTO="mlnx_ofed_name_end"
61+
SUBSYSTEM!="net", GOTO="mlnx_ofed_name_end"
62+
63+
# Rename physical interfaces (first case) of virtual functions (second case).
64+
# Example names:
65+
# enp8s0f0np0 -> enp8s0f0
66+
# enp8s0f0np1v12 -> enp8s0f0v12
67+
68+
DRIVERS=="mlx5_core", ENV{ID_NET_NAME_PATH}!="", \
69+
PROGRAM="/bin/sh -c 'echo $env{ID_NET_NAME_PATH} | sed -r -e s/np[01]$// -e s/np[01]v/v/'", \
70+
ENV{ID_NET_NAME_PATH}="$result"
71+
72+
DRIVERS=="mlx5_core", ENV{ID_NET_NAME_SLOT}!="", \
73+
PROGRAM="/bin/sh -c 'echo $env{ID_NET_NAME_SLOT} | sed -r -e s/np[01]$// -e s/np[01]v/v/'", \
74+
ENV{ID_NET_NAME_SLOT}="$result"
75+
76+
LABEL="mlnx_ofed_name_end"
77+
`
78+
79+
// Write the udev rules file
80+
if err := u.os.WriteFile(u.path, []byte(udevRulesContent), 0o644); err != nil {
81+
log.Error(err, "failed to create udev rules file", "path", u.path)
82+
return err
83+
}
84+
85+
log.Info("udev rules file created successfully", "path", u.path)
86+
87+
// Read and log the file content on debug level (equivalent to bash: debug_print `cat ${MLX_UDEV_RULES_FILE}`)
88+
if content, err := u.os.ReadFile(u.path); err != nil {
89+
log.Error(err, "failed to read udev rules file for debug output", "path", u.path)
90+
} else {
91+
log.V(1).Info("udev rules file content", "path", u.path, "content", string(content))
92+
}
93+
5594
return nil
5695
}
5796

5897
// RemoveRules is the default implementation of the udev.Interface.
5998
func (u *udev) RemoveRules(ctx context.Context) error {
60-
_ = logr.FromContextOrDiscard(ctx)
61-
// TODO add implementation
99+
log := logr.FromContextOrDiscard(ctx)
100+
log.Info("remove udev rules")
101+
102+
// Check if the udev rules file exists
103+
_, err := u.os.Stat(u.path)
104+
if err != nil {
105+
// Check if it's a "file not found" error
106+
if os.IsNotExist(err) {
107+
// File doesn't exist, log and skip
108+
log.Info("udev rules file was not previously created, skipping", "path", u.path)
109+
return nil
110+
}
111+
// Other errors (permission denied, etc.) should be returned
112+
log.Error(err, "failed to check if udev rules file exists", "path", u.path)
113+
return err
114+
}
115+
116+
// File exists, delete it
117+
log.Info("deleting udev rules", "path", u.path)
118+
if err := u.os.RemoveAll(u.path); err != nil {
119+
log.Error(err, "failed to remove udev rules file", "path", u.path)
120+
return err
121+
}
122+
123+
log.Info("udev rules file deleted successfully", "path", u.path)
62124
return nil
63125
}
64126

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2025, NVIDIA CORPORATION & AFFILIATES
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+
17+
package udev
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestUdev(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Udev Suite")
29+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
Copyright 2025, NVIDIA CORPORATION & AFFILIATES
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+
17+
package udev
18+
19+
import (
20+
"context"
21+
"os"
22+
"time"
23+
24+
. "github.com/onsi/ginkgo/v2"
25+
. "github.com/onsi/gomega"
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/mock"
28+
29+
osMockPkg "github.com/Mellanox/doca-driver-build/entrypoint/internal/wrappers/mocks"
30+
)
31+
32+
// mockFileInfo is a simple mock implementation of os.FileInfo
33+
type mockFileInfo struct{}
34+
35+
func (m mockFileInfo) Name() string { return "mock" }
36+
func (m mockFileInfo) Size() int64 { return 0 }
37+
func (m mockFileInfo) Mode() os.FileMode { return 0 }
38+
func (m mockFileInfo) ModTime() time.Time { return time.Now() }
39+
func (m mockFileInfo) IsDir() bool { return false }
40+
func (m mockFileInfo) Sys() interface{} { return nil }
41+
42+
var _ = Describe("Udev", func() {
43+
Context("CreateRules", func() {
44+
var (
45+
u Interface
46+
osMock *osMockPkg.OSWrapper
47+
testPath string
48+
)
49+
50+
BeforeEach(func() {
51+
osMock = osMockPkg.NewOSWrapper(GinkgoT())
52+
testPath = "/host/etc/udev/rules.d/77-mlnx-net-names.rules"
53+
u = New(testPath, osMock)
54+
})
55+
56+
It("should create udev rules file successfully", func() {
57+
expectedContent := `ACTION!="add", GOTO="mlnx_ofed_name_end"
58+
SUBSYSTEM!="net", GOTO="mlnx_ofed_name_end"
59+
60+
# Rename physical interfaces (first case) of virtual functions (second case).
61+
# Example names:
62+
# enp8s0f0np0 -> enp8s0f0
63+
# enp8s0f0np1v12 -> enp8s0f0v12
64+
65+
DRIVERS=="mlx5_core", ENV{ID_NET_NAME_PATH}!="", \
66+
PROGRAM="/bin/sh -c 'echo $env{ID_NET_NAME_PATH} | sed -r -e s/np[01]$// -e s/np[01]v/v/'", \
67+
ENV{ID_NET_NAME_PATH}="$result"
68+
69+
DRIVERS=="mlx5_core", ENV{ID_NET_NAME_SLOT}!="", \
70+
PROGRAM="/bin/sh -c 'echo $env{ID_NET_NAME_SLOT} | sed -r -e s/np[01]$// -e s/np[01]v/v/'", \
71+
ENV{ID_NET_NAME_SLOT}="$result"
72+
73+
LABEL="mlnx_ofed_name_end"
74+
`
75+
76+
osMock.EXPECT().WriteFile(testPath, []byte(expectedContent), os.FileMode(0o644)).Return(nil)
77+
osMock.EXPECT().ReadFile(testPath).Return([]byte(expectedContent), nil)
78+
79+
err := u.CreateRules(context.Background())
80+
Expect(err).ToNot(HaveOccurred())
81+
})
82+
83+
It("should handle file creation failure", func() {
84+
osMock.EXPECT().WriteFile(testPath, mock.AnythingOfType("[]uint8"), os.FileMode(0o644)).Return(assert.AnError)
85+
86+
err := u.CreateRules(context.Background())
87+
Expect(err).To(HaveOccurred())
88+
Expect(err).To(Equal(assert.AnError))
89+
})
90+
91+
It("should handle different file paths", func() {
92+
customPath := "/custom/path/to/udev/rules.d/99-custom.rules"
93+
u = New(customPath, osMock)
94+
95+
osMock.EXPECT().WriteFile(customPath, mock.AnythingOfType("[]uint8"), os.FileMode(0o644)).Return(nil)
96+
osMock.EXPECT().ReadFile(customPath).Return([]byte("mock content"), nil)
97+
98+
err := u.CreateRules(context.Background())
99+
Expect(err).ToNot(HaveOccurred())
100+
})
101+
102+
It("should handle nested directory paths", func() {
103+
nestedPath := "/host/etc/udev/rules.d/nested/deep/path/77-mlnx-net-names.rules"
104+
u = New(nestedPath, osMock)
105+
106+
osMock.EXPECT().WriteFile(nestedPath, mock.AnythingOfType("[]uint8"), os.FileMode(0o644)).Return(nil)
107+
osMock.EXPECT().ReadFile(nestedPath).Return([]byte("mock content"), nil)
108+
109+
err := u.CreateRules(context.Background())
110+
Expect(err).ToNot(HaveOccurred())
111+
})
112+
113+
It("should create rules with correct content structure", func() {
114+
var capturedContent []byte
115+
osMock.EXPECT().WriteFile(testPath, mock.AnythingOfType("[]uint8"), os.FileMode(0o644)).Run(func(name string, data []byte, perm os.FileMode) {
116+
capturedContent = data
117+
}).Return(nil)
118+
osMock.EXPECT().ReadFile(testPath).Return(capturedContent, nil)
119+
120+
err := u.CreateRules(context.Background())
121+
Expect(err).ToNot(HaveOccurred())
122+
123+
content := string(capturedContent)
124+
Expect(content).To(ContainSubstring(`ACTION!="add", GOTO="mlnx_ofed_name_end"`))
125+
Expect(content).To(ContainSubstring(`SUBSYSTEM!="net", GOTO="mlnx_ofed_name_end"`))
126+
Expect(content).To(ContainSubstring(`DRIVERS=="mlx5_core", ENV{ID_NET_NAME_PATH}!=""`))
127+
Expect(content).To(ContainSubstring(`DRIVERS=="mlx5_core", ENV{ID_NET_NAME_SLOT}!=""`))
128+
Expect(content).To(ContainSubstring(`LABEL="mlnx_ofed_name_end"`))
129+
Expect(content).To(ContainSubstring(`sed -r -e s/np[01]$// -e s/np[01]v/v/`))
130+
})
131+
132+
It("should handle multiple calls to CreateRules", func() {
133+
osMock.EXPECT().WriteFile(testPath, mock.AnythingOfType("[]uint8"), os.FileMode(0o644)).Return(nil).Times(2)
134+
osMock.EXPECT().ReadFile(testPath).Return([]byte("mock content"), nil).Times(2)
135+
136+
err := u.CreateRules(context.Background())
137+
Expect(err).ToNot(HaveOccurred())
138+
139+
err = u.CreateRules(context.Background())
140+
Expect(err).ToNot(HaveOccurred())
141+
})
142+
143+
It("should handle ReadFile failure gracefully", func() {
144+
osMock.EXPECT().WriteFile(testPath, mock.AnythingOfType("[]uint8"), os.FileMode(0o644)).Return(nil)
145+
osMock.EXPECT().ReadFile(testPath).Return(nil, assert.AnError)
146+
147+
err := u.CreateRules(context.Background())
148+
Expect(err).ToNot(HaveOccurred()) // ReadFile failure should not cause CreateRules to fail
149+
})
150+
})
151+
152+
Context("RemoveRules", func() {
153+
var (
154+
u Interface
155+
osMock *osMockPkg.OSWrapper
156+
testPath string
157+
)
158+
159+
BeforeEach(func() {
160+
osMock = osMockPkg.NewOSWrapper(GinkgoT())
161+
testPath = "/host/etc/udev/rules.d/77-mlnx-net-names.rules"
162+
u = New(testPath, osMock)
163+
})
164+
165+
It("should remove udev rules file when it exists", func() {
166+
// Mock file exists check
167+
osMock.EXPECT().Stat(testPath).Return(mockFileInfo{}, nil)
168+
osMock.EXPECT().RemoveAll(testPath).Return(nil)
169+
170+
err := u.RemoveRules(context.Background())
171+
Expect(err).ToNot(HaveOccurred())
172+
})
173+
174+
It("should handle file not existing gracefully", func() {
175+
// Mock file doesn't exist (Stat returns os.ErrNotExist)
176+
osMock.EXPECT().Stat(testPath).Return(nil, os.ErrNotExist)
177+
178+
err := u.RemoveRules(context.Background())
179+
Expect(err).ToNot(HaveOccurred())
180+
})
181+
182+
It("should handle file removal failure", func() {
183+
// Mock file exists but removal fails
184+
osMock.EXPECT().Stat(testPath).Return(mockFileInfo{}, nil)
185+
osMock.EXPECT().RemoveAll(testPath).Return(assert.AnError)
186+
187+
err := u.RemoveRules(context.Background())
188+
Expect(err).To(HaveOccurred())
189+
Expect(err).To(Equal(assert.AnError))
190+
})
191+
192+
It("should handle different file paths", func() {
193+
customPath := "/custom/path/to/udev/rules.d/99-custom.rules"
194+
u = New(customPath, osMock)
195+
196+
osMock.EXPECT().Stat(customPath).Return(mockFileInfo{}, nil)
197+
osMock.EXPECT().RemoveAll(customPath).Return(nil)
198+
199+
err := u.RemoveRules(context.Background())
200+
Expect(err).ToNot(HaveOccurred())
201+
})
202+
203+
It("should handle nested directory paths", func() {
204+
nestedPath := "/host/etc/udev/rules.d/nested/deep/path/77-mlnx-net-names.rules"
205+
u = New(nestedPath, osMock)
206+
207+
osMock.EXPECT().Stat(nestedPath).Return(mockFileInfo{}, nil)
208+
osMock.EXPECT().RemoveAll(nestedPath).Return(nil)
209+
210+
err := u.RemoveRules(context.Background())
211+
Expect(err).ToNot(HaveOccurred())
212+
})
213+
214+
It("should handle multiple calls to RemoveRules", func() {
215+
// First call - file exists
216+
osMock.EXPECT().Stat(testPath).Return(mockFileInfo{}, nil)
217+
osMock.EXPECT().RemoveAll(testPath).Return(nil)
218+
219+
err := u.RemoveRules(context.Background())
220+
Expect(err).ToNot(HaveOccurred())
221+
222+
// Second call - file no longer exists
223+
osMock.EXPECT().Stat(testPath).Return(nil, os.ErrNotExist)
224+
225+
err = u.RemoveRules(context.Background())
226+
Expect(err).ToNot(HaveOccurred())
227+
})
228+
229+
It("should handle Stat error other than file not found", func() {
230+
// Mock Stat returning a different error (e.g., permission denied)
231+
osMock.EXPECT().Stat(testPath).Return(nil, assert.AnError)
232+
233+
err := u.RemoveRules(context.Background())
234+
Expect(err).To(HaveOccurred())
235+
Expect(err).To(Equal(assert.AnError))
236+
})
237+
})
238+
})

0 commit comments

Comments
 (0)