diff --git a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/attribute.go b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/attribute.go new file mode 100644 index 00000000000..068bead167d --- /dev/null +++ b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/attribute.go @@ -0,0 +1,52 @@ +/* +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 +} diff --git a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/attribute_test.go b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/attribute_test.go new file mode 100644 index 00000000000..0d5e6dba976 --- /dev/null +++ b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/attribute_test.go @@ -0,0 +1,112 @@ +/* +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" + "reflect" + "strings" + "testing" + + "k8s.io/utils/ptr" + + resourceapi "k8s.io/dynamic-resource-allocation/api" +) + +func TestAddStandardDeviceAttributes(t *testing.T) { + testPCIBusID := "0123:45:1e.7" + testACIIntermediateBusID := "0123:46:7.1" + testPCIRoot := "pci0123:56" + + tests := map[string]struct { + mockSysfsSetup func(t *testing.T, mockSysfs sysfs) + opts []StandardDeviceAttributesOption + expectedAttrs map[resourceapi.QualifiedName]resourceapi.DeviceAttribute + expectsError bool + expectedErrMsg string + }{ + "valid nil attrs": { + opts: nil, + expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}, + }, + "valid empty attrs": { + opts: []StandardDeviceAttributesOption{}, + expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}, + }, + "valid empty pci attrs": { + opts: []StandardDeviceAttributesOption{WithStandardPCIDeviceAttributesOpts()}, + expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}, + }, + "valid pci attrs": { + mockSysfsSetup: func(t *testing.T, mockSysfs sysfs) { + // /sys/bus/pci/devices/0123:45:1e.7 --> /sys/devices/pci0123:56/0123:46:7.1/10123:45:1e.7 + testPCIDevicePath := mockSysfs.Devices(filepath.Join(testPCIRoot, testACIIntermediateBusID, testPCIBusID)) + TouchFile(t, testPCIDevicePath) + testPCIBusPath := mockSysfs.Bus(filepath.Join("pci", "devices", testPCIBusID)) + CreateSymlink(t, testPCIDevicePath, testPCIBusPath) + }, + opts: []StandardDeviceAttributesOption{ + WithStandardPCIDeviceAttributesOpts( + WithPCIDeviceAddress(testPCIBusID), + ), + }, + expectedAttrs: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{ + qualifiedNamePCIeRoot: { + StringValue: ptr.To(testPCIRoot), + }, + }, + }, + "invalid empty pciBusID": { + opts: []StandardDeviceAttributesOption{ + WithStandardPCIDeviceAttributesOpts( + WithPCIDeviceAddress(""), + ), + }, + expectsError: true, + expectedErrMsg: "PCI Bus ID cannot be empty", + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockSysfsPath := t.TempDir() + + mockSysfs := sysfs(mockSysfsPath) + if test.mockSysfsSetup != nil { + test.mockSysfsSetup(t, mockSysfs) + test.opts = append(test.opts, WithStandardPCIDeviceAttributesOpts(withSysfs(mockSysfs))) + } + + attrs, err := StandardDeviceAttributes(test.opts...) + + if test.expectsError { + if err == nil { + t.Errorf("expected error but got nil") + return + } + if !strings.Contains(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(attrs, test.expectedAttrs) { + t.Errorf("expected attributes %v, got %v", test.expectedAttrs, attrs) + return + } + }) + } +} diff --git a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci.go b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci.go index 9b20430b295..041751f1989 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci.go @@ -28,16 +28,66 @@ import ( var ( bdfRegexp = regexp.MustCompile(`^([0-9a-f]{4}):([0-9a-f]{2}):([0-9a-f]{2})\.([0-9a-f]{1})$`) + + qualifiedNamePCIeRoot = resourceapi.QualifiedName(resourceapi.StandardDeviceAttributePCIeRoot) ) -// GetPCIeRootAttributeByPCIBusID retrieves the PCIe Root Complex for a given PCI Bus ID. +type StandardPCIDeviceAttributesOption func(args *StandardPCIDeviceAttributeArgs) + +// WithPCIDeviceAddress sets the PCI address for the 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:") // 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) { +func WithPCIDeviceAddress(pciBusID string) StandardPCIDeviceAttributesOption { + return func(args *StandardPCIDeviceAttributeArgs) { + args.pciAddress = pciBusID + } +} + +// StandardPCIDeviceAttributeArgs holds the arguments for generating standard PCI device attributes. +type StandardPCIDeviceAttributeArgs struct { + pciAddress string + sysfs sysfs +} + +// StandardPCIDeviceAttributes returns standard device attributes for a PCI device. +func StandardPCIDeviceAttributes(opts ...StandardPCIDeviceAttributesOption) (map[resourceapi.QualifiedName]resourceapi.DeviceAttribute, error) { + args := &StandardPCIDeviceAttributeArgs{ + sysfs: sysfs(defaultSysfsRoot), + } + for _, opt := range opts { + opt(args) + } + + attrs := make(map[resourceapi.QualifiedName]resourceapi.DeviceAttribute) + + pciRootAttribute, err := getPCIeRootAttributeByPCIBusID(args.pciAddress, args.sysfs) + if err != nil { + return nil, err + } + attrs[qualifiedNamePCIeRoot] = pciRootAttribute + + return attrs, nil +} + +// This is only for testing purposes. +func withSysfs(sysfs sysfs) StandardPCIDeviceAttributesOption { + return func(args *StandardPCIDeviceAttributeArgs) { + args.sysfs = sysfs + } +} + +// GetPCIeRootAttributeByPCI Bus ID 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:") +// 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, sysfs sysfs) (resourceapi.DeviceAttribute, error) { if pciBusID == "" { return resourceapi.DeviceAttribute{}, fmt.Errorf("PCI Bus ID cannot be empty") } @@ -47,7 +97,7 @@ func GetPCIeRootAttributeByPCIBusID(pciBusID string) (resourceapi.DeviceAttribut } // e.g. /sys/devices/pci0000:01/....../0000:00:1f.0, - sysDevicesPath, err := resolveSysDevicesPath(pciBusID) + sysDevicesPath, err := resolveSysDevicesPath(pciBusID, sysfs) if err != nil { return resourceapi.DeviceAttribute{}, fmt.Errorf("failed to resolve sysfs path for PCI Bus ID %s: %w", pciBusID, err) } @@ -73,7 +123,7 @@ func GetPCIeRootAttributeByPCIBusID(pciBusID string) (resourceapi.DeviceAttribut // For example, if the PCIAddress is "0000:00:1f.0", // /sys/bus/pci/devices/0000:00:1f.0 points to // /sys/devices/pci0000:01/....../0000:00:1f.0, -func resolveSysDevicesPath(pciBusID string) (string, error) { +func resolveSysDevicesPath(pciBusID string, sysfs sysfs) (string, error) { // e.g. /sys/bus/pci/devices/0000:00:1f.0 sysBusPath := sysfs.Bus(filepath.Join("pci", "devices", pciBusID)) diff --git a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci_test.go b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci_test.go index 28a5fa65ed4..449420d66a9 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci_test.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/pci_test.go @@ -28,78 +28,83 @@ import ( resourceapi "k8s.io/dynamic-resource-allocation/api" ) -func TestGetPCIeRootBAttributeyPCIBusID(t *testing.T) { +func TestStandardPCIDeviceAttributes(t *testing.T) { pciBusID := "0000:01:02.3" + pciIntermediateBusID := "0000:00:13.1" pcieRoot := "pci0000:01" - expectedAttribute := &resourceapi.DeviceAttribute{ - StringValue: ptr.To(pcieRoot), - } - tests := map[string]struct { - smockSysfsSetup func(t *testing.T, mockSysfs sysfsPath) - address string - expectedAttribute *resourceapi.DeviceAttribute - expectsError bool - expectedErrMsg string + smockSysfsSetup func(t *testing.T, mockSysfs sysfs) + opts []StandardPCIDeviceAttributesOption + expected map[resourceapi.QualifiedName]resourceapi.DeviceAttribute + expectsError bool + expectedErrMsg string }{ "valid": { - smockSysfsSetup: func(t *testing.T, mockSysfs sysfsPath) { - devicePath := mockSysfs.Devices(filepath.Join(pcieRoot, "0000:00:13.1", pciBusID)) + smockSysfsSetup: func(t *testing.T, mockSysfs sysfs) { + devicePath := mockSysfs.Devices(filepath.Join(pcieRoot, pciIntermediateBusID, pciBusID)) TouchFile(t, devicePath) busPath := mockSysfs.Bus(filepath.Join("pci", "devices", pciBusID)) CreateSymlink(t, devicePath, busPath) }, - address: pciBusID, - expectedAttribute: expectedAttribute, - expectsError: false, + opts: []StandardPCIDeviceAttributesOption{ + WithPCIDeviceAddress(pciBusID), + }, + expected: map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{ + qualifiedNamePCIeRoot: { + StringValue: ptr.To(pcieRoot), + }, + }, }, "invalid empty PCI Bus ID": { - address: "", - expectedAttribute: nil, - expectsError: true, - expectedErrMsg: "PCI Bus ID cannot be empty", + opts: []StandardPCIDeviceAttributesOption{ + WithPCIDeviceAddress(""), + }, + expected: nil, + expectsError: true, + expectedErrMsg: "PCI Bus ID cannot be empty", }, "invalid PCI Bus ID format": { - address: "invalid-pci-id", - expectedAttribute: nil, - expectsError: true, - expectedErrMsg: "invalid PCI Bus ID format: invalid-pci-id", + opts: []StandardPCIDeviceAttributesOption{ + WithPCIDeviceAddress("invalid-pci-id"), + }, + expected: nil, + expectsError: true, + expectedErrMsg: "invalid PCI Bus ID format: invalid-pci-id", }, "invalid no exist PCI Bus ID": { - address: pciBusID, - expectedAttribute: nil, - expectsError: true, - expectedErrMsg: "no such file or directory", + opts: []StandardPCIDeviceAttributesOption{ + WithPCIDeviceAddress(pciBusID), + }, + expected: nil, + expectsError: true, + expectedErrMsg: "no such file or directory", }, "invalid symlink": { - smockSysfsSetup: func(t *testing.T, mockSysfs sysfsPath) { + smockSysfsSetup: func(t *testing.T, mockSysfs sysfs) { 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) }, - address: pciBusID, - expectedAttribute: nil, - expectsError: true, - expectedErrMsg: "invalid symlink target for PCI Bus ID", + opts: []StandardPCIDeviceAttributesOption{ + WithPCIDeviceAddress(pciBusID), + }, + expected: nil, + expectsError: true, + expectedErrMsg: "invalid symlink target for PCI Bus ID", }, } + for name, test := range tests { t.Run(name, func(t *testing.T) { mockSysfsPath := t.TempDir() - mockSysfs := sysfsPath(mockSysfsPath) + + mockSysfs := sysfs(mockSysfsPath) if test.smockSysfsSetup != nil { test.smockSysfsSetup(t, mockSysfs) } - sysfs = mockSysfs - t.Cleanup(func() { - sysfs = sysfsPath(sysfsRoot) - if err := os.RemoveAll(mockSysfsPath); err != nil { - t.Errorf("Failed to clean up mock sysfs path %s: %v", mockSysfsPath, err) - } - }) - got, err := GetPCIeRootAttributeByPCIBusID(test.address) + got, err := StandardPCIDeviceAttributes(append(test.opts, withSysfs(mockSysfs))...) if test.expectsError { if err == nil { t.Errorf("Expected error but got none") @@ -114,8 +119,8 @@ func TestGetPCIeRootBAttributeyPCIBusID(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %v", err) } - if !reflect.DeepEqual(got, *test.expectedAttribute) { - t.Errorf("Expected attribute %v, got %v", test.expectedAttribute, got) + if !reflect.DeepEqual(got, test.expected) { + t.Errorf("Expected attributes %v, got %v", test.expected, got) } }) } diff --git a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/sysfs.go b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/sysfs.go index 0c81f72998d..93ad9c475fd 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/sysfs.go +++ b/staging/src/k8s.io/dynamic-resource-allocation/deviceattribute/sysfs.go @@ -19,45 +19,41 @@ package deviceattribute import "path/filepath" const ( - sysfsRoot = "/sys" + defaultSysfsRoot = "/sys" ) -var ( - sysfs sysfsPath = sysfsPath(sysfsRoot) -) - -// sysfsPath provides methods to construct sysfs paths for various subsystems. +// sysfs 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 +type sysfs string -func (s sysfsPath) Devices(path string) string { +func (s sysfs) Devices(path string) string { return filepath.Join(string(s), "devices", path) } -func (s sysfsPath) Bus(path string) string { +func (s sysfs) Bus(path string) string { return filepath.Join(string(s), "bus", path) } -func (s sysfsPath) Block(path string) string { +func (s sysfs) Block(path string) string { return filepath.Join(string(s), "block", path) } -func (s sysfsPath) Class(path string) string { +func (s sysfs) Class(path string) string { return filepath.Join(string(s), "class", path) } -func (s sysfsPath) Dev(path string) string { +func (s sysfs) Dev(path string) string { return filepath.Join(string(s), "dev", path) } -func (s sysfsPath) Firmware(path string) string { +func (s sysfs) Firmware(path string) string { return filepath.Join(string(s), "firmware", path) } -func (s sysfsPath) Kernel(path string) string { +func (s sysfs) Kernel(path string) string { return filepath.Join(string(s), "kernel", path) } -func (s sysfsPath) Module(path string) string { +func (s sysfs) Module(path string) string { return filepath.Join(string(s), "module", path) }