Simplify the implementation: just provide GetPCIeRootAttributeByPCIBusID

This commit is contained in:
Shingo Omura
2025-06-17 22:58:17 +09:00
parent 59f0ab97c2
commit a8ab9eb5fe
9 changed files with 205 additions and 1143 deletions

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}
})
}
}

View File

@@ -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)
}