mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-23 19:16:25 +00:00
Simplify the implementation: just provide GetPCIeRootAttributeByPCIBusID
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 deviceattribute
|
||||
|
||||
import resourceapi "k8s.io/dynamic-resource-allocation/api"
|
||||
|
||||
type StandardDeviceAttributesOption func(args *StandardDeviceAttributesOptions)
|
||||
|
||||
// WithStandardPCIDeviceAttributesOpts sets the standard PCI device attributes options
|
||||
func WithStandardPCIDeviceAttributesOpts(opts ...StandardPCIDeviceAttributesOption) StandardDeviceAttributesOption {
|
||||
return func(args *StandardDeviceAttributesOptions) {
|
||||
args.pciDeviceAttributeOpts = append(args.pciDeviceAttributeOpts, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
type StandardDeviceAttributesOptions struct {
|
||||
pciDeviceAttributeOpts []StandardPCIDeviceAttributesOption
|
||||
}
|
||||
|
||||
// StandardDeviceAttributes generates standard device attributes based on the provided options.
|
||||
// It currently supports standard PCI device attributes if options are provided.
|
||||
func StandardDeviceAttributes(
|
||||
opts ...StandardDeviceAttributesOption,
|
||||
) (map[resourceapi.QualifiedName]resourceapi.DeviceAttribute, error) {
|
||||
options := &StandardDeviceAttributesOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
attrs := map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
|
||||
|
||||
if len(options.pciDeviceAttributeOpts) > 0 {
|
||||
// Standard PCI device attributes
|
||||
pciAttrs, err := StandardPCIDeviceAttributes(options.pciDeviceAttributeOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range pciAttrs {
|
||||
attrs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return attrs, nil
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 deviceattribute
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
resourceapi "k8s.io/dynamic-resource-allocation/api"
|
||||
dratesting "k8s.io/dynamic-resource-allocation/testing"
|
||||
"k8s.io/dynamic-resource-allocation/utils"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestAddStandardDeviceAttributes(t *testing.T) {
|
||||
// Create mock sysfs for testing
|
||||
testSysfsDir, err := os.MkdirTemp("", "test-sysfs")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp sysfs directory: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(testSysfsDir); err != nil {
|
||||
t.Fatalf("failed to clean up temp sysfs directory: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
testSysFs := utils.NewSysfsWithRoot(testSysfsDir)
|
||||
// "0123:45:1e.7"
|
||||
testPCIAddress := utils.MustNewPCIAddress(0x0123, 0x45, 0x1e, 0x7)
|
||||
// "1234:56"
|
||||
testPCIRoot := utils.MustNewPCIRoot(0x1234, 0x56)
|
||||
|
||||
// /sys/bus/pci/devices/0123:45:1e.7 --> /sys/devices/pci1234:56/0123:45:1e.7
|
||||
testPCIDevicePath := testSysFs.Devices("pci" + testPCIRoot.String() + "/" + testPCIAddress.String())
|
||||
dratesting.TouchFile(t, testPCIDevicePath)
|
||||
testPCIBusPath := testSysFs.Bus("pci/devices/" + testPCIAddress.String())
|
||||
dratesting.CreateSymlink(t, testPCIDevicePath, testPCIBusPath)
|
||||
|
||||
tests := map[string]struct {
|
||||
opts []StandardDeviceAttributesOption
|
||||
expectedAttrs map[resourceapi.QualifiedName]resourceapi.DeviceAttribute
|
||||
}{
|
||||
"pci attrs": {
|
||||
opts: []StandardDeviceAttributesOption{
|
||||
WithStandardPCIDeviceAttributesOpts(
|
||||
WithPCIDeviceAddress(testPCIAddress),
|
||||
withSysfs(testSysFs), // Use the mock sysfs
|
||||
),
|
||||
},
|
||||
expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||
qualifiedNamePCIeRoot: {
|
||||
StringValue: ptr.To("pci" + testPCIRoot.String()),
|
||||
},
|
||||
},
|
||||
},
|
||||
"nil attrs": {
|
||||
opts: nil,
|
||||
expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{},
|
||||
},
|
||||
"empty attrs": {
|
||||
opts: []StandardDeviceAttributesOption{},
|
||||
expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{},
|
||||
},
|
||||
"empty pci attrs": {
|
||||
opts: []StandardDeviceAttributesOption{WithStandardPCIDeviceAttributesOpts()},
|
||||
expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
attrs, err := StandardDeviceAttributes(test.opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(attrs, test.expectedAttrs) {
|
||||
t.Errorf("expected %v, got %v", test.expectedAttrs, attrs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,62 +17,83 @@ limitations under the License.
|
||||
package deviceattribute
|
||||
|
||||
import (
|
||||
"k8s.io/utils/ptr"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
resourceapi "k8s.io/dynamic-resource-allocation/api"
|
||||
utils "k8s.io/dynamic-resource-allocation/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
qualifiedNamePCIeRoot = resourceapi.QualifiedName(resourceapi.StandardDeviceAttributePCIeRoot)
|
||||
bdfRegexp = regexp.MustCompile(`^([0-9a-f]{4}):([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]{1})$`)
|
||||
)
|
||||
|
||||
type StandardPCIDeviceAttributesOption func(args *StandardPCIDeviceAttributeArgs)
|
||||
|
||||
// WithPCIDeviceAddress sets the PCI address for the PCI device attributes.
|
||||
func WithPCIDeviceAddress(pciAddress *utils.PCIAddress) StandardPCIDeviceAttributesOption {
|
||||
return func(args *StandardPCIDeviceAttributeArgs) {
|
||||
args.pciAddress = pciAddress
|
||||
}
|
||||
}
|
||||
|
||||
// StandardPCIDeviceAttributeArgs holds the arguments for generating standard PCI device attributes.
|
||||
type StandardPCIDeviceAttributeArgs struct {
|
||||
pciAddress *utils.PCIAddress
|
||||
sysfs utils.Sysfs
|
||||
}
|
||||
|
||||
// StandardPCIDeviceAttributes returns standard device attributes for a PCI device.
|
||||
func StandardPCIDeviceAttributes(opts ...StandardPCIDeviceAttributesOption) (map[resourceapi.QualifiedName]resourceapi.DeviceAttribute, error) {
|
||||
args := &StandardPCIDeviceAttributeArgs{
|
||||
sysfs: utils.NewSysfs(),
|
||||
// GetPCIeRootAttributeByPCIBusID retrieves the PCIe Root Complex for a given PCI Bus ID.
|
||||
// in BDF (Bus-Device-Function) format, e.g., "0123:45:1e.7".
|
||||
//
|
||||
// It returns a DeviceAttribute with the PCIe Root Complex information("pci<domain>:<bus>")
|
||||
// as a string value or an error if the PCI Bus ID is invalid or the root complex cannot be determined.
|
||||
//
|
||||
// ref: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
|
||||
func GetPCIeRootAttributeByPCIBusID(pciBusID string) (resourceapi.DeviceAttribute, error) {
|
||||
if pciBusID == "" {
|
||||
return resourceapi.DeviceAttribute{}, fmt.Errorf("PCI Bus ID cannot be empty")
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(args)
|
||||
if !bdfRegexp.MatchString(pciBusID) {
|
||||
return resourceapi.DeviceAttribute{}, fmt.Errorf("invalid PCI Bus ID format: %s", pciBusID)
|
||||
}
|
||||
|
||||
if args.pciAddress == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attrs := make(map[resourceapi.QualifiedName]resourceapi.DeviceAttribute)
|
||||
|
||||
pciRoot, err := args.pciAddress.ResolvePCIRoot(args.sysfs)
|
||||
// e.g. /sys/devices/pci0000:01/...<intermediate PCI devices>.../0000:00:1f.0,
|
||||
sysDevicesPath, err := resolveSysDevicesPath(pciBusID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return resourceapi.DeviceAttribute{}, fmt.Errorf("failed to resolve sysfs path for PCI Bus ID %s: %w", pciBusID, err)
|
||||
}
|
||||
|
||||
attrs[qualifiedNamePCIeRoot] = resourceapi.DeviceAttribute{
|
||||
StringValue: ptr.To("pci" + pciRoot.String()),
|
||||
}
|
||||
pciRootPart := strings.Split(strings.TrimPrefix(sysDevicesPath, sysfs.Devices("")+"/"), "/")[0]
|
||||
|
||||
return attrs, nil
|
||||
return resourceapi.DeviceAttribute{
|
||||
StringValue: &pciRootPart,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This is only for testing purposes.
|
||||
func withSysfs(sysfs utils.Sysfs) StandardPCIDeviceAttributesOption {
|
||||
return func(args *StandardPCIDeviceAttributeArgs) {
|
||||
args.sysfs = sysfs
|
||||
// resolveSysDevicesPath resolves the /sys/devices path from the PCI Bus ID
|
||||
// in BDF (Bus-Device-Function) format, e.g., "0123:45:1e.7".
|
||||
//
|
||||
// ref: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
|
||||
//
|
||||
// /sys/devices has directory structure which reflects the hardware hierarchy in the system.
|
||||
// Therefore, the device path may contains intermediate directories(devices).
|
||||
// Thus, we can not simply find the device path from the PCIAddress.
|
||||
// Fortunately, /sys/bus/pci/devices/<address> is a symlink to the actual device path in /sys/devices.
|
||||
// So we can resolve the actual device path by reading the symlink at /sys/bus/pci/devices/<address>.
|
||||
//
|
||||
// For example, if the PCIAddress is "0000:00:1f.0",
|
||||
// /sys/bus/pci/devices/0000:00:1f.0 points to
|
||||
// /sys/devices/pci0000:01/...<intermediate PCI devices>.../0000:00:1f.0,
|
||||
func resolveSysDevicesPath(pciBusID string) (string, error) {
|
||||
// e.g. /sys/bus/pci/devices/0000:00:1f.0
|
||||
sysBusPath := sysfs.Bus(filepath.Join("pci", "devices", pciBusID))
|
||||
|
||||
targetRelative, err := os.Readlink(sysBusPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read symlink for PCI Bus ID %s: %w", sysBusPath, err)
|
||||
}
|
||||
var targetAbs string
|
||||
if filepath.IsAbs(targetRelative) {
|
||||
targetAbs = targetRelative
|
||||
} else {
|
||||
// If the target is a relative path, we need to resolve it relative to the symlink's directory.
|
||||
targetAbs = filepath.Join(filepath.Dir(sysBusPath), targetRelative)
|
||||
}
|
||||
|
||||
// targetAbs must be /sys/devices/pci0000:01/...<intermediate PCI devices>.../0000:00:1f.0
|
||||
devicePathPrefix := sysfs.Devices("pci")
|
||||
if !strings.HasPrefix(targetAbs, devicePathPrefix) || filepath.Base(targetAbs) != pciBusID {
|
||||
return "", fmt.Errorf("invalid symlink target for PCI Bus ID %s: %s", sysBusPath, targetAbs)
|
||||
}
|
||||
|
||||
return targetAbs, nil
|
||||
}
|
||||
|
||||
@@ -26,133 +26,117 @@ import (
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
resourceapi "k8s.io/dynamic-resource-allocation/api"
|
||||
dratesting "k8s.io/dynamic-resource-allocation/testing"
|
||||
utils "k8s.io/dynamic-resource-allocation/utils"
|
||||
)
|
||||
|
||||
func TestStandardPCIDeviceAttributes(t *testing.T) {
|
||||
pcieRootAttrName := resourceapi.QualifiedName(resourceapi.StandardDeviceAttributePCIeRoot)
|
||||
|
||||
address := utils.MustNewPCIAddress(0x0, 0x1, 0x2, 0x3)
|
||||
root := utils.MustNewPCIRoot(0x0, 0x1)
|
||||
standardAttributes := map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||
pcieRootAttrName: {
|
||||
StringValue: ptr.To("pci" + root.String()),
|
||||
},
|
||||
func TestGetPCIeRootBAttributeyPCIBusID(t *testing.T) {
|
||||
pciBusID := "0000:01:02.3"
|
||||
pcieRoot := "pci0000:01"
|
||||
expectedAttribute := &resourceapi.DeviceAttribute{
|
||||
StringValue: ptr.To(pcieRoot),
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
pciAddress *utils.PCIAddress
|
||||
sysfs func(*testing.T, string) utils.Sysfs
|
||||
expectedAttributes map[resourceapi.QualifiedName]resourceapi.DeviceAttribute
|
||||
expectsErr bool
|
||||
expectedErrMsg string
|
||||
smockSysfsSetup func(t *testing.T, mockSysfs sysfsPath)
|
||||
address string
|
||||
expectedAttribute *resourceapi.DeviceAttribute
|
||||
expectsError bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
"nil address": {
|
||||
pciAddress: nil,
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) utils.Sysfs {
|
||||
return utils.NewSysfs()
|
||||
"valid": {
|
||||
smockSysfsSetup: func(t *testing.T, mockSysfs sysfsPath) {
|
||||
devicePath := mockSysfs.Devices(filepath.Join(pcieRoot, "0000:00:13.1", pciBusID))
|
||||
TouchFile(t, devicePath)
|
||||
busPath := mockSysfs.Bus(filepath.Join("pci", "devices", pciBusID))
|
||||
CreateSymlink(t, devicePath, busPath)
|
||||
},
|
||||
expectedAttributes: nil,
|
||||
address: pciBusID,
|
||||
expectedAttribute: expectedAttribute,
|
||||
expectsError: false,
|
||||
},
|
||||
"valid address": {
|
||||
pciAddress: address,
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) utils.Sysfs {
|
||||
sysfs := utils.NewSysfsWithRoot(testSysfsRoot)
|
||||
devicePath := sysfs.Devices(filepath.Join("pci"+root.String(), address.String()))
|
||||
dratesting.TouchFile(t, devicePath)
|
||||
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.CreateSymlink(t, devicePath, busPath)
|
||||
return sysfs
|
||||
},
|
||||
expectedAttributes: standardAttributes,
|
||||
"invalid empty PCI Bus ID": {
|
||||
address: "",
|
||||
expectedAttribute: nil,
|
||||
expectsError: true,
|
||||
expectedErrMsg: "PCI Bus ID cannot be empty",
|
||||
},
|
||||
"non-existent address": {
|
||||
pciAddress: address,
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) utils.Sysfs {
|
||||
return utils.NewSysfsWithRoot(testSysfsRoot)
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "no such file or directory",
|
||||
"invalid PCI Bus ID format": {
|
||||
address: "invalid-pci-id",
|
||||
expectedAttribute: nil,
|
||||
expectsError: true,
|
||||
expectedErrMsg: "invalid PCI Bus ID format: invalid-pci-id",
|
||||
},
|
||||
"devices path not a symlink": {
|
||||
pciAddress: address,
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) utils.Sysfs {
|
||||
sysfs := utils.NewSysfsWithRoot(testSysfsRoot)
|
||||
devicePath := sysfs.Devices(filepath.Join("pci"+root.String(), address.String()))
|
||||
dratesting.TouchFile(t, devicePath)
|
||||
|
||||
// Create a file instead of a symlink
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.TouchFile(t, busPath)
|
||||
return sysfs
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "invalid argument",
|
||||
"invalid no exist PCI Bus ID": {
|
||||
address: pciBusID,
|
||||
expectedAttribute: nil,
|
||||
expectsError: true,
|
||||
expectedErrMsg: "no such file or directory",
|
||||
},
|
||||
"invalid symlink target": {
|
||||
pciAddress: address,
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) utils.Sysfs {
|
||||
sysfs := utils.NewSysfsWithRoot(testSysfsRoot)
|
||||
invalidDevicePath := sysfs.Devices(filepath.Join("invalid", "pci"+root.String(), address.String()))
|
||||
dratesting.TouchFile(t, invalidDevicePath)
|
||||
|
||||
// Create a symlink that points to an invalid path
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.CreateSymlink(t, invalidDevicePath, busPath)
|
||||
return sysfs
|
||||
"invalid symlink": {
|
||||
smockSysfsSetup: func(t *testing.T, mockSysfs sysfsPath) {
|
||||
devicePath := mockSysfs.Devices(filepath.Join("invalid-pci-root", "0000:00:13.1", pciBusID))
|
||||
TouchFile(t, devicePath)
|
||||
busPath := mockSysfs.Bus(filepath.Join("pci", "devices", pciBusID))
|
||||
CreateSymlink(t, devicePath, busPath)
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "invalid symlink target for PCI device",
|
||||
},
|
||||
"invalid root format": {
|
||||
pciAddress: address,
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) utils.Sysfs {
|
||||
sysfs := utils.NewSysfsWithRoot(testSysfsRoot)
|
||||
devicePath := sysfs.Devices(filepath.Join("pci-invalid-root", address.String()))
|
||||
dratesting.TouchFile(t, devicePath)
|
||||
|
||||
// Create a symlink with an invalid root format
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.CreateSymlink(t, devicePath, busPath)
|
||||
return sysfs
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "invalid PCIRoot format",
|
||||
address: pciBusID,
|
||||
expectedAttribute: nil,
|
||||
expectsError: true,
|
||||
expectedErrMsg: "invalid symlink target for PCI Bus ID",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testSysfsRoot, err := os.MkdirTemp("", "sysfs-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp sysfs directory: %v", err)
|
||||
mockSysfsPath := t.TempDir()
|
||||
mockSysfs := sysfsPath(mockSysfsPath)
|
||||
if test.smockSysfsSetup != nil {
|
||||
test.smockSysfsSetup(t, mockSysfs)
|
||||
}
|
||||
sysfs = mockSysfs
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(testSysfsRoot); err != nil {
|
||||
t.Errorf("Failed to remove temp sysfs directory: %v", err)
|
||||
sysfs = sysfsPath(sysfsRoot)
|
||||
if err := os.RemoveAll(mockSysfsPath); err != nil {
|
||||
t.Errorf("Failed to clean up mock sysfs path %s: %v", mockSysfsPath, err)
|
||||
}
|
||||
})
|
||||
result, err := StandardPCIDeviceAttributes(
|
||||
WithPCIDeviceAddress(test.pciAddress),
|
||||
withSysfs(test.sysfs(t, testSysfsRoot)),
|
||||
)
|
||||
if test.expectsErr {
|
||||
|
||||
got, err := GetPCIeRootAttributeByPCIBusID(test.address)
|
||||
if test.expectsError {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but got none")
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.expectedErrMsg) {
|
||||
t.Fatalf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
|
||||
t.Errorf("Expected error message to contain %q, got %q", test.expectedErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.expectedAttributes) {
|
||||
t.Fatalf("Expected attributes %v, got %v", test.expectedAttributes, result)
|
||||
if !reflect.DeepEqual(got, *test.expectedAttribute) {
|
||||
t.Errorf("Expected attribute %v, got %v", test.expectedAttribute, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TouchFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatalf("Failed to create directory %s: %v", filepath.Dir(path), err)
|
||||
}
|
||||
if _, err := os.Create(path); err != nil {
|
||||
t.Fatalf("Failed to create file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSymlink(t *testing.T, target, link string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(link), 0755); err != nil {
|
||||
t.Fatalf("Failed to create directory for symlink %s: %v", filepath.Dir(link), err)
|
||||
}
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("Failed to create symlink from %s to %s: %v", target, link, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 deviceattribute
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
const (
|
||||
sysfsRoot = "/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
sysfs sysfsPath = sysfsPath(sysfsRoot)
|
||||
)
|
||||
|
||||
// sysfsPath provides methods to construct sysfs paths for various subsystems.
|
||||
// It is used to abstract the sysfs path construction
|
||||
// and can be replaced with a mock in tests.
|
||||
type sysfsPath string
|
||||
|
||||
func (s sysfsPath) Devices(path string) string {
|
||||
return filepath.Join(string(s), "devices", path)
|
||||
}
|
||||
|
||||
func (s sysfsPath) Bus(path string) string {
|
||||
return filepath.Join(string(s), "bus", path)
|
||||
}
|
||||
|
||||
func (s sysfsPath) Block(path string) string {
|
||||
return filepath.Join(string(s), "block", path)
|
||||
}
|
||||
|
||||
func (s sysfsPath) Class(path string) string {
|
||||
return filepath.Join(string(s), "class", path)
|
||||
}
|
||||
|
||||
func (s sysfsPath) Dev(path string) string {
|
||||
return filepath.Join(string(s), "dev", path)
|
||||
}
|
||||
|
||||
func (s sysfsPath) Firmware(path string) string {
|
||||
return filepath.Join(string(s), "firmware", path)
|
||||
}
|
||||
func (s sysfsPath) Kernel(path string) string {
|
||||
return filepath.Join(string(s), "kernel", path)
|
||||
}
|
||||
|
||||
func (s sysfsPath) Module(path string) string {
|
||||
return filepath.Join(string(s), "module", path)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 testing
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TouchFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatalf("Failed to create directory %s: %v", filepath.Dir(path), err)
|
||||
}
|
||||
if _, err := os.Create(path); err != nil {
|
||||
t.Fatalf("Failed to create file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateSymlink(t *testing.T, target, link string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(link), 0755); err != nil {
|
||||
t.Fatalf("Failed to create directory for symlink %s: %v", filepath.Dir(link), err)
|
||||
}
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("Failed to create symlink from %s to %s: %v", target, link, err)
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// PCI address consists of:
|
||||
// - domain: 16 bits, it can be represented in 4-digit hexadecimal number
|
||||
// - bus: 8 bits, it can be represented in 2-digit hexadecimal number
|
||||
// - device: 5 bits, it can be represented in 2-digit hexadecimal number
|
||||
// - function: 3 bits, it can be represented in 1-digit hexadecimal number
|
||||
//
|
||||
// ref: "Chapter 12. PCI Drivers - PCI Addressing"
|
||||
// Linux Device Drivers, 3rd Edition by Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman
|
||||
// https://www.oreilly.com/library/view/linux-device-drivers/0596005903/ch12.html
|
||||
PCIDomainBits = uint16(16)
|
||||
PCIDomainMax = uint16(math.MaxUint16)
|
||||
PCIBusBits = uint16(8)
|
||||
PCIBusMax = uint16((1 << PCIBusBits) - 1)
|
||||
PCIDeviceBits = uint16(5)
|
||||
PCIDeviceMax = uint16((1 << PCIDeviceBits) - 1)
|
||||
PCIFunctionBits = uint16(3)
|
||||
PCIFunctionMax = uint16((1 << PCIFunctionBits) - 1)
|
||||
)
|
||||
|
||||
var (
|
||||
// bdfRegexp matches PCI address in BDF notation.
|
||||
// The format is <domain>:<bus>:<device>.<function>
|
||||
// where:
|
||||
// - domain: 4-digit hexadecimal number representing the PCI domain (16 bits)
|
||||
// - bus: 2-digit hexadecimal number representing the PCI bus (8 bits)
|
||||
// - device: 2-digit hexadecimal number representing the PCI device (5 bits)
|
||||
// - function: 1-digit hexadecimal number representing the PCI function (3 bits)
|
||||
//
|
||||
// Example: "0000:0e:1f.0" represents domain 0, bus 14, device 31, function 0.
|
||||
//
|
||||
// ref: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
|
||||
bdfRegexp = regexp.MustCompile(`^([0-9a-f]{4}):([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]{1})$`)
|
||||
|
||||
// pciRootRegexp matches PCI root address in the format "pci<domain>:<bus>".
|
||||
// The format is:
|
||||
// - domain: 4-digit hexadecimal number representing the PCI domain (32 bits)
|
||||
// - bus: 2-digit hexadecimal number representing the PCI bus (8 bits)
|
||||
pciRootRegexp = regexp.MustCompile(`^([0-9a-f]{4}):([0-9a-f]{2})$`)
|
||||
)
|
||||
|
||||
// PCIRoot represents a PCI root address in the combination of Domain and Bus.
|
||||
// This represents the top-level element in sysfs PCI device hierarchy.
|
||||
// (i.e. /sys/devices/pci<domain>:<bus>)
|
||||
//
|
||||
// ref: https://docs.kernel.org/PCI/sysfs-pci.html
|
||||
type PCIRoot struct {
|
||||
domain uint16
|
||||
bus uint16
|
||||
}
|
||||
|
||||
func NewPCIRoot(domain, bus uint16) (*PCIRoot, error) {
|
||||
// no validation for domain, as it uses full 16 bits
|
||||
|
||||
if bus > PCIBusMax {
|
||||
return nil, fmt.Errorf("invalid PCI bus number: %02x, must be in range 0-%d (%dbits)", bus, PCIBusMax, PCIBusBits)
|
||||
}
|
||||
return &PCIRoot{
|
||||
domain: domain,
|
||||
bus: bus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MustNewPCIRoot(domain, bus uint16) *PCIRoot {
|
||||
root, err := NewPCIRoot(domain, bus)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create PCIRoot: %v", err))
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// String returns the string representation of the PCIRoot in the format "domain:bus".
|
||||
func (p *PCIRoot) String() string {
|
||||
return fmt.Sprintf("%04x:%02x", p.domain, p.bus)
|
||||
}
|
||||
|
||||
// The PCIAddress holds PCI address components in BDF notation.
|
||||
// <domain>:<bus>:<device>.<function>
|
||||
// where:
|
||||
// - domain: 4-digit hexadecimal number representing the PCI domain (16 bits)
|
||||
// - bus: 2-digit hexadecimal number representing the PCI bus (8 bits)
|
||||
// - device: 2-digit hexadecimal number representing the PCI device (5 bits)
|
||||
// - function: 1-digit hexadecimal number representing the PCI function (3 bits)
|
||||
//
|
||||
// Example: "0000:0e:1f.0" represents domain 0, bus 14, device 31, function 0.
|
||||
//
|
||||
// ref:
|
||||
// - BDF Notation: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
|
||||
// - PCI Addressing: https://docs.kernel.org/PCI/sysfs-pci.html
|
||||
type PCIAddress struct {
|
||||
domain uint16
|
||||
bus uint16
|
||||
device uint16
|
||||
function uint16
|
||||
}
|
||||
|
||||
func NewPCIAddress(domain, bus, device, function uint16) (*PCIAddress, error) {
|
||||
// no validation for domain, as it uses full 16 bits
|
||||
if bus > PCIBusMax {
|
||||
return nil, fmt.Errorf("invalid PCI bus number: %02x, must be in range 0-%d (%dbits)", bus, PCIBusMax, PCIBusBits)
|
||||
}
|
||||
if device > PCIDeviceMax {
|
||||
return nil, fmt.Errorf("invalid PCI device number: %02x, must be in range 0-%d (%dbits)", device, PCIDeviceMax, PCIDeviceBits)
|
||||
}
|
||||
if function > PCIFunctionMax {
|
||||
return nil, fmt.Errorf("invalid PCI function number: %01x, must be in range 0-%d (%dbits)", function, PCIFunctionMax, PCIFunctionBits)
|
||||
}
|
||||
return &PCIAddress{
|
||||
domain: domain,
|
||||
bus: bus,
|
||||
device: device,
|
||||
function: function,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MustNewPCIAddress(domain, bus, device, function uint16) *PCIAddress {
|
||||
addr, err := NewPCIAddress(domain, bus, device, function)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create PCIAddress: %v", err))
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func (p *PCIAddress) String() string {
|
||||
return fmt.Sprintf("%04x:%02x:%02x.%01x", p.domain, p.bus, p.device, p.function)
|
||||
}
|
||||
|
||||
// ResolvePCIRoot resolves the PCI root for the PCIAddress.
|
||||
// It returns a PCIRoot object that contains the domain and bus of the PCI root.
|
||||
// The PCIRoot is derived from the sysfs path of the PCIAddress.
|
||||
//
|
||||
// sysfs argument is optional and only for testing purposes.
|
||||
func (p *PCIAddress) ResolvePCIRoot(sysfs Sysfs) (*PCIRoot, error) {
|
||||
if sysfs == nil {
|
||||
sysfs = NewSysfs()
|
||||
}
|
||||
|
||||
// e.g. /sys/devices/pci0000:01/...<intermediate PCI devices>.../0000:00:1f.0,
|
||||
sysDevicesPath, err := p.resolveSysDevicesPath(sysfs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve sysfs path for PCI device %s: %w", p.String(), err)
|
||||
}
|
||||
|
||||
pciRootPart := strings.Split(strings.TrimPrefix(sysDevicesPath, sysfs.Devices("")+"/"), "/")[0]
|
||||
pciRoot, err := ParsePCIRoot(
|
||||
pciRootPart[3:], // skip "pci" prefix
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve PCI root for %s: %w", p.String(), err)
|
||||
}
|
||||
return pciRoot, nil
|
||||
}
|
||||
|
||||
// resolveSysDevicesPath resolves the /sys/devices path for the PCIAddress.
|
||||
//
|
||||
// /sys/devices has directory structure which reflects the hardware hierarchy in the system.
|
||||
// Therefore, the device path may contains intermediate directories(devices).
|
||||
// Thus, we can not simply find the device path from the PCIAddress.
|
||||
// Fortunately, /sys/bus/pci/devices/<address> is a symlink to the actual device path in /sys/devices.
|
||||
// So we can resolve the actual device path by reading the symlink at /sys/bus/pci/devices/<address>.
|
||||
//
|
||||
// For example, if the PCIAddress is "0000:00:1f.0",
|
||||
// /sys/bus/pci/devices/0000:00:1f.0 points to
|
||||
// /sys/devices/pci0000:01/...<intermediate PCI devices>.../0000:00:1f.0,
|
||||
func (p *PCIAddress) resolveSysDevicesPath(sysfs Sysfs) (string, error) {
|
||||
// e.g. /sys/bus/pci/devices/0000:00:1f.0
|
||||
sysBusPath := sysfs.Bus(filepath.Join("pci", "devices", p.String()))
|
||||
|
||||
targetRelative, err := os.Readlink(sysBusPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read symlink for PCI device %s: %w", sysBusPath, err)
|
||||
}
|
||||
var targetAbs string
|
||||
if filepath.IsAbs(targetRelative) {
|
||||
targetAbs = targetRelative
|
||||
} else {
|
||||
// If the target is a relative path, we need to resolve it relative to the symlink's directory.
|
||||
targetAbs = filepath.Join(filepath.Dir(sysBusPath), targetRelative)
|
||||
}
|
||||
|
||||
// targetAbs must be /sys/devices/pci0000:01/...<intermediate PCI devices>.../0000:00:1f.0
|
||||
devicePathPrefix := sysfs.Devices("pci")
|
||||
if !strings.HasPrefix(targetAbs, devicePathPrefix) || filepath.Base(targetAbs) != p.String() {
|
||||
return "", fmt.Errorf("invalid symlink target for PCI device %s: %s", sysBusPath, targetAbs)
|
||||
}
|
||||
|
||||
return targetAbs, nil
|
||||
}
|
||||
|
||||
// ParsePCIRoot parses a PCIRoot from a string in the format "domain:bus".
|
||||
// The format is:
|
||||
// - domain: 4-digit hexadecimal number representing the PCI domain
|
||||
// - bus: 2-digit hexadecimal number representing the PCI bus
|
||||
// Example: "0000:01" represents domain 0, bus 1.
|
||||
func ParsePCIRoot(str string) (*PCIRoot, error) {
|
||||
match := pciRootRegexp.FindStringSubmatch(str)
|
||||
|
||||
if len(match) == 0 {
|
||||
return nil, fmt.Errorf("invalid PCIRoot format: %s", str)
|
||||
}
|
||||
|
||||
parsePart := func(name, part string) (uint16, error) {
|
||||
value, err := parseHexTouint16(part)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid value %s in PCIRoot %s: %w", name, str, err)
|
||||
}
|
||||
return uint16(value), nil
|
||||
}
|
||||
|
||||
var domain, bus uint16
|
||||
var err error
|
||||
if domain, err = parsePart("domain", match[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bus, err = parsePart("bus", match[2]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewPCIRoot(domain, bus)
|
||||
}
|
||||
|
||||
// ParsePCIAddress parses a PCI address in BDF notation.
|
||||
// The format is <domain>:<bus>:<device>.<function>
|
||||
// where:
|
||||
// - domain: 4-digit hexadecimal number representing the PCI domain
|
||||
// - bus: 2-digit hexadecimal number representing the PCI bus
|
||||
// - device: 2-digit hexadecimal number representing the PCI device
|
||||
// - function: 1-digit hexadecimal number representing the PCI function
|
||||
// The function returns a PCIAddress object or an error if the format is invalid.
|
||||
//
|
||||
// Example: "0000:0e:1f.0" represents domain 0, bus 14, device 31, function 0.
|
||||
//
|
||||
// ref: https://wiki.xenproject.org/wiki/Bus:Device.Function_(BDF)_Notation
|
||||
func ParsePCIAddress(bdfString string) (*PCIAddress, error) {
|
||||
match := bdfRegexp.FindStringSubmatch(bdfString)
|
||||
|
||||
if len(match) == 0 {
|
||||
return nil, fmt.Errorf("invalid PCI address format: %s", bdfString)
|
||||
}
|
||||
|
||||
parsePart := func(name, part string) (uint16, error) {
|
||||
value, err := parseHexTouint16(part)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid value %s in PCI address %s: %w", name, bdfString, err)
|
||||
}
|
||||
return uint16(value), nil
|
||||
}
|
||||
|
||||
var domain, bus, device, function uint16
|
||||
var err error
|
||||
if domain, err = parsePart("domain", match[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bus, err = parsePart("bus", match[2]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if device, err = parsePart("device", match[3]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if function, err = parsePart("function", match[4]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewPCIAddress(domain, bus, device, function)
|
||||
}
|
||||
|
||||
func parseHexTouint16(s string) (uint16, error) {
|
||||
value, err := strconv.ParseUint(s, 16, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint16(value), nil
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
dratesting "k8s.io/dynamic-resource-allocation/testing"
|
||||
)
|
||||
|
||||
func TestNewPCIAddress(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
domain, bus, device, function uint16
|
||||
expectsErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
"valid zero address": {0, 0, 0, 0, false, ""},
|
||||
"valid simple address": {1, 2, 3, 4, false, ""},
|
||||
"valid complex address": {0x1234, 0x56, 0x1e, 0x5, false, ""},
|
||||
// no invalid domain case because domain uses full 16 bits
|
||||
"invalid bus": {0, PCIBusMax + 1, 0, 0, true, "invalid PCI bus number"},
|
||||
"invalid device": {0, 0, PCIDeviceMax + 1, 0, true, "invalid PCI device number"},
|
||||
"invalid function": {0, 0, 0, PCIFunctionMax + 1, true, "invalid PCI function number"},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
address, err := NewPCIAddress(test.domain, test.bus, test.device, test.function)
|
||||
if test.expectsErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.expectedErrMsg) {
|
||||
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if address.domain != test.domain || address.bus != test.bus || address.device != test.device || address.function != test.function {
|
||||
t.Errorf("Expected PCIAddress(%d, %d, %d, %d), got (%d, %d, %d, %d)",
|
||||
test.domain, test.bus, test.device, test.function,
|
||||
address.domain, address.bus, address.device, address.function)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCIAddressString(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
address *PCIAddress
|
||||
expected string
|
||||
}{
|
||||
"zero": {MustNewPCIAddress(0, 0, 0, 0), "0000:00:00.0"},
|
||||
"simple": {MustNewPCIAddress(0x0001, 0x02, 0x03, 0x4), "0001:02:03.4"},
|
||||
"complex": {MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x5), "1234:56:1e.5"},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got := test.address.String(); got != test.expected {
|
||||
t.Errorf("Expected %s, got %s", test.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIAddress(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
expected *PCIAddress
|
||||
expectsErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
"valid zero": {"0000:00:00.0", MustNewPCIAddress(0x0, 0x0, 0x0, 0x0), false, ""},
|
||||
"valid simple": {"0001:02:03.4", MustNewPCIAddress(0x1, 0x2, 0x3, 0x4), false, ""},
|
||||
"valid complex": {"1234:56:1e.7", MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x7), false, ""},
|
||||
"invalid format": {"0000-00-00-0", nil, true, "invalid PCI address forma"},
|
||||
"too less parts": {"0000:00:00", nil, true, "invalid PCI address forma"},
|
||||
"too many parts": {"0000:00:00.0.1", nil, true, "invalid PCI address forma"},
|
||||
"invalid hex value": {"0000:00:00.0g", nil, true, "invalid PCI address format"},
|
||||
"completely invalid": {"invalid", nil, true, "invalid PCI address forma"},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result, err := ParsePCIAddress(test.input)
|
||||
if test.expectsErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none for input %s", test.input)
|
||||
return
|
||||
}
|
||||
if test.expectedErrMsg != "" && !strings.Contains(err.Error(), test.expectedErrMsg) {
|
||||
t.Errorf("Expected error message %q contains %q for input %s", err.Error(), test.expectedErrMsg, test.input)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for input %s: %v", test.input, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("ParsePCIAddress(%s) = %+v, want %+v", test.input, result, test.expected)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIAddressRoundTrip(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
addressStr string
|
||||
expected *PCIAddress
|
||||
}{
|
||||
"zero": {"0000:00:00.0", MustNewPCIAddress(0x0, 0x0, 0x0, 0x0)},
|
||||
"simple": {"0001:02:03.4", MustNewPCIAddress(0x1, 0x2, 0x3, 0x4)},
|
||||
"complex": {"1234:56:1e.5", MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x5)},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
address, err := ParsePCIAddress(test.addressStr)
|
||||
if err != nil {
|
||||
t.Errorf("ParsePCIAddress(%s) returned error: %v", test.addressStr, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(address, test.expected) {
|
||||
t.Errorf("ParsePCIAddress(%s) = %+v, want %+v", test.addressStr, address, test.expected)
|
||||
return
|
||||
}
|
||||
if address.String() != test.expected.String() {
|
||||
t.Errorf("Expected %s, got %s", test.expected.String(), address.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPCIRoot(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
domain uint16
|
||||
bus uint16
|
||||
address *PCIRoot
|
||||
expectsErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
"valid zero root": {0, 0, MustNewPCIRoot(0x0, 0x0), false, ""},
|
||||
"valid simple root": {1, 2, MustNewPCIRoot(0x1, 0x2), false, ""},
|
||||
"valid complex root": {0x1234, 0x56, MustNewPCIRoot(0x1234, 0x56), false, ""},
|
||||
// no invalid domain case because domain uses full 16 bits
|
||||
"invalid bus": {0, PCIBusMax + 1, nil, true, "invalid PCI bus number"},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
root, err := NewPCIRoot(test.domain, test.bus)
|
||||
if test.expectsErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.expectedErrMsg) {
|
||||
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if root.String() != test.address.String() {
|
||||
t.Errorf("Expected PCIRoot %s, got %s", test.address.String(), root.String())
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCIRootString(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
root *PCIRoot
|
||||
expected string
|
||||
}{
|
||||
"zero": {MustNewPCIRoot(0x0, 0x0), "0000:00"},
|
||||
"simple": {MustNewPCIRoot(0x1, 0x2), "0001:02"},
|
||||
"complex": {MustNewPCIRoot(0x1234, 0x56), "1234:56"},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got := test.root.String(); got != test.expected {
|
||||
t.Errorf("Expected %s, got %s", test.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIRoot(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
expected *PCIRoot
|
||||
expectsErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
"valid zero": {"0000:00", MustNewPCIRoot(0x0, 0x0), false, ""},
|
||||
"valid simple": {"0001:02", MustNewPCIRoot(0x1, 0x2), false, ""},
|
||||
"valid complex": {"1234:56", MustNewPCIRoot(0x1234, 0x56), false, ""},
|
||||
"invalid format": {"0000-00", nil, true, "invalid PCIRoot format"},
|
||||
"invalid too less parts": {"0000", nil, true, "invalid PCIRoot format"},
|
||||
"invalid too many parts": {"0000:00:00", nil, true, "invalid PCIRoot format"},
|
||||
"invalid hex value": {"0000:0g", nil, true, "invalid PCIRoot format"},
|
||||
"invalid completely invalid": {"invalid", nil, true, "invalid PCIRoot format"},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result, err := ParsePCIRoot(test.input)
|
||||
if test.expectsErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if test.expectedErrMsg != "" && !strings.Contains(err.Error(), test.expectedErrMsg) {
|
||||
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("Expected PCIRoot %s, got %s", test.expected.String(), result.String())
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIRootRoundTrip(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
rootStr string
|
||||
expected *PCIRoot
|
||||
}{
|
||||
"zero": {"0000:00", MustNewPCIRoot(0x0, 0x0)},
|
||||
"simple": {"0001:02", MustNewPCIRoot(0x1, 0x2)},
|
||||
"complex": {"1234:56", MustNewPCIRoot(0x1234, 0x56)},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
root, err := ParsePCIRoot(test.rootStr)
|
||||
if err != nil {
|
||||
t.Errorf("ParsePCIRoot(%s) returned error: %v", test.rootStr, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(root, test.expected) {
|
||||
t.Errorf("ParsePCIRoot(%s) = %+v, want %+v", test.rootStr, root, test.expected)
|
||||
return
|
||||
}
|
||||
if root.String() != test.expected.String() {
|
||||
t.Errorf("Expected %s, got %s", test.expected.String(), root.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCIAddressResolvePCIRoot(t *testing.T) {
|
||||
address := MustNewPCIAddress(0x1234, 0x56, 0x1e, 0x7)
|
||||
root := MustNewPCIRoot(0x2345, 0x67)
|
||||
|
||||
tests := map[string]struct {
|
||||
sysfs func(*testing.T, string) Sysfs
|
||||
expectsErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
"valid address": {
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) Sysfs {
|
||||
sysfs := NewSysfsWithRoot(testSysfsRoot)
|
||||
devicePath := sysfs.Devices(filepath.Join("pci"+root.String(), address.String()))
|
||||
dratesting.TouchFile(t, devicePath)
|
||||
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.CreateSymlink(t, devicePath, busPath)
|
||||
return sysfs
|
||||
},
|
||||
},
|
||||
"non-existent address": {
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) Sysfs {
|
||||
return NewSysfsWithRoot(testSysfsRoot)
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "no such file or directory",
|
||||
},
|
||||
"devices path not a symlink": {
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) Sysfs {
|
||||
sysfs := NewSysfsWithRoot(testSysfsRoot)
|
||||
devicePath := sysfs.Devices(filepath.Join("pci"+root.String(), address.String()))
|
||||
dratesting.TouchFile(t, devicePath)
|
||||
|
||||
// Create a file instead of a symlink
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.TouchFile(t, busPath)
|
||||
return sysfs
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "invalid argument",
|
||||
},
|
||||
"invalid symlink target": {
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) Sysfs {
|
||||
sysfs := NewSysfsWithRoot(testSysfsRoot)
|
||||
invalidDevicePath := sysfs.Devices(filepath.Join("invalid", "pci"+root.String(), address.String()))
|
||||
dratesting.TouchFile(t, invalidDevicePath)
|
||||
|
||||
// Create a symlink that points to an invalid path
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.CreateSymlink(t, invalidDevicePath, busPath)
|
||||
return sysfs
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "invalid symlink target for PCI device",
|
||||
},
|
||||
"invalid root format": {
|
||||
sysfs: func(t *testing.T, testSysfsRoot string) Sysfs {
|
||||
sysfs := NewSysfsWithRoot(testSysfsRoot)
|
||||
devicePath := sysfs.Devices(filepath.Join("pci-invalid-root", address.String()))
|
||||
dratesting.TouchFile(t, devicePath)
|
||||
|
||||
// Create a symlink with an invalid root format
|
||||
busPath := sysfs.Bus(filepath.Join("pci", "devices", address.String()))
|
||||
dratesting.CreateSymlink(t, devicePath, busPath)
|
||||
return sysfs
|
||||
},
|
||||
expectsErr: true,
|
||||
expectedErrMsg: "invalid PCIRoot format",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testSysfsRoot, err := os.MkdirTemp("", "sysfs_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(testSysfsRoot); err != nil {
|
||||
t.Errorf("Failed to remove temp sysfs directory: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
ret, err := address.ResolvePCIRoot(test.sysfs(t, testSysfsRoot))
|
||||
|
||||
if test.expectsErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.expectedErrMsg) {
|
||||
t.Errorf("Expected error message %q contains %q", err.Error(), test.expectedErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if ret == nil {
|
||||
t.Errorf("Expected non-nil PCIRoot, got nil")
|
||||
return
|
||||
}
|
||||
if ret.String() != root.String() {
|
||||
t.Errorf("Expected PCIRoot %s, got %s", root.String(), ret.String())
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 utils
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
const (
|
||||
sysfsPath = "/sys"
|
||||
)
|
||||
|
||||
// Sysfs provides methods to access various sysfs paths.
|
||||
// It abstracts the sysfs directory structure, allowing for easy retrieval of paths
|
||||
// ref: https://linux-kernel-labs.github.io/refs/heads/master/labs/device_model.html#sysfs
|
||||
type Sysfs interface {
|
||||
// Devices returns the full path to the devices directory with the specified path appended.
|
||||
// The path is relative to /sys/devices.
|
||||
Devices(path string) string
|
||||
|
||||
// Bus returns the full path to the bus directory with the specified path appended.
|
||||
// The path is relative to /sys/bus.
|
||||
Bus(path string) string
|
||||
|
||||
// Block returns the full path to the block directory with the specified path appended.
|
||||
// The path is relative to /sys/block.
|
||||
Block(path string) string
|
||||
|
||||
// Class returns the full path to the class directory with the specified path appended.
|
||||
// The path is relative to /sys/class.
|
||||
Class(path string) string
|
||||
|
||||
// Dev returns the full path to the dev directory with the specified path appended.
|
||||
// The path is relative to /sys/dev.
|
||||
Dev(path string) string
|
||||
|
||||
// Firmware returns the full path to the firmware directory with the specified path appended.
|
||||
// The path is relative to /sys/firmware.
|
||||
Firmware(path string) string
|
||||
|
||||
// Kernel returns the full path to the kernel directory with the specified path appended.
|
||||
// The path is relative to /sys/kernel.
|
||||
Kernel(path string) string
|
||||
|
||||
// Module returns the full path to the module directory with the specified path appended.
|
||||
// The path is relative to /sys/module.
|
||||
Module(path string) string
|
||||
}
|
||||
|
||||
// sysfs provides methods to access sysfs paths
|
||||
// root is the root path of the sysfs filesystem, typically "/sys".
|
||||
// Setting non-"/sys" root is only for testing purposes.
|
||||
type sysfs struct {
|
||||
root string
|
||||
}
|
||||
|
||||
// NewSysfs creates a new Sysfs instance(/sys)
|
||||
func NewSysfs() Sysfs {
|
||||
return &sysfs{root: sysfsPath}
|
||||
}
|
||||
|
||||
// NewSysfsWithRoot creates a new Sysfs instance with the specified root path.
|
||||
// If the root path is empty, it defaults to "/sys".
|
||||
// This method is only for testing purposes.
|
||||
func NewSysfsWithRoot(root string) Sysfs {
|
||||
if root == "" {
|
||||
root = sysfsPath
|
||||
}
|
||||
return &sysfs{root: root}
|
||||
}
|
||||
|
||||
func (s *sysfs) Devices(path string) string {
|
||||
return filepath.Join(s.root, "devices", path)
|
||||
}
|
||||
|
||||
func (s *sysfs) Bus(path string) string {
|
||||
return filepath.Join(s.root, "bus", path)
|
||||
}
|
||||
|
||||
func (s *sysfs) Block(path string) string {
|
||||
return filepath.Join(s.root, "block", path)
|
||||
}
|
||||
|
||||
func (s *sysfs) Class(path string) string {
|
||||
return filepath.Join(s.root, "class", path)
|
||||
}
|
||||
|
||||
func (s *sysfs) Dev(path string) string {
|
||||
return filepath.Join(s.root, "dev", path)
|
||||
}
|
||||
|
||||
func (s *sysfs) Firmware(path string) string {
|
||||
return filepath.Join(s.root, "firmware", path)
|
||||
}
|
||||
func (s *sysfs) Kernel(path string) string {
|
||||
return filepath.Join(s.root, "kernel", path)
|
||||
}
|
||||
|
||||
func (s *sysfs) Module(path string) string {
|
||||
return filepath.Join(s.root, "module", path)
|
||||
}
|
||||
Reference in New Issue
Block a user