| // Copyright The Prometheus 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. |
| |
| //go:build linux |
| |
| package sysfs |
| |
| import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/prometheus/procfs/internal/util" |
| ) |
| |
| // PciPowerState represents the power state of a PCI device. |
| type PciPowerState string |
| |
| const ( |
| PciPowerStateUnknown PciPowerState = "unknown" |
| PciPowerStateError PciPowerState = "error" |
| PciPowerStateD0 PciPowerState = "D0" |
| PciPowerStateD1 PciPowerState = "D1" |
| PciPowerStateD2 PciPowerState = "D2" |
| PciPowerStateD3Hot PciPowerState = "D3hot" |
| PciPowerStateD3Cold PciPowerState = "D3cold" |
| ) |
| |
| // String returns the string representation of the power state. |
| func (p PciPowerState) String() string { |
| return string(p) |
| } |
| |
| const pciDevicesPath = "bus/pci/devices" |
| |
| // PciDeviceLocation represents the location of the device attached. |
| // "0000:00:00.0" represents Segment:Bus:Device.Function . |
| type PciDeviceLocation struct { |
| Segment int |
| Bus int |
| Device int |
| Function int |
| } |
| |
| func (pdl PciDeviceLocation) String() string { |
| return fmt.Sprintf("%04x:%02x:%02x:%x", pdl.Segment, pdl.Bus, pdl.Device, pdl.Function) |
| } |
| |
| func (pdl PciDeviceLocation) Strings() []string { |
| return []string{ |
| fmt.Sprintf("%04x", pdl.Segment), |
| fmt.Sprintf("%02x", pdl.Bus), |
| fmt.Sprintf("%02x", pdl.Device), |
| fmt.Sprintf("%x", pdl.Function), |
| } |
| } |
| |
| // PciDevice contains info from files in /sys/bus/pci/devices for a |
| // single PCI device. |
| type PciDevice struct { |
| Location PciDeviceLocation |
| ParentLocation *PciDeviceLocation |
| |
| Class uint32 // /sys/bus/pci/devices/<Location>/class |
| Vendor uint32 // /sys/bus/pci/devices/<Location>/vendor |
| Device uint32 // /sys/bus/pci/devices/<Location>/device |
| SubsystemVendor uint32 // /sys/bus/pci/devices/<Location>/subsystem_vendor |
| SubsystemDevice uint32 // /sys/bus/pci/devices/<Location>/subsystem_device |
| Revision uint32 // /sys/bus/pci/devices/<Location>/revision |
| |
| NumaNode *int32 // /sys/bus/pci/devices/<Location>/numa_node |
| |
| MaxLinkSpeed *float64 // /sys/bus/pci/devices/<Location>/max_link_speed |
| MaxLinkWidth *float64 // /sys/bus/pci/devices/<Location>/max_link_width |
| CurrentLinkSpeed *float64 // /sys/bus/pci/devices/<Location>/current_link_speed |
| CurrentLinkWidth *float64 // /sys/bus/pci/devices/<Location>/current_link_width |
| |
| SriovDriversAutoprobe *bool // /sys/bus/pci/devices/<Location>/sriov_drivers_autoprobe |
| SriovNumvfs *uint32 // /sys/bus/pci/devices/<Location>/sriov_numvfs |
| SriovOffset *uint32 // /sys/bus/pci/devices/<Location>/sriov_offset |
| SriovStride *uint32 // /sys/bus/pci/devices/<Location>/sriov_stride |
| SriovTotalvfs *uint32 // /sys/bus/pci/devices/<Location>/sriov_totalvfs |
| SriovVfDevice *uint32 // /sys/bus/pci/devices/<Location>/sriov_vf_device |
| SriovVfTotalMsix *uint64 // /sys/bus/pci/devices/<Location>/sriov_vf_total_msix |
| |
| D3coldAllowed *bool // /sys/bus/pci/devices/<Location>/d3cold_allowed |
| PowerState *PciPowerState // /sys/bus/pci/devices/<Location>/power_state |
| } |
| |
| func (pd PciDevice) Name() string { |
| return pd.Location.String() |
| } |
| |
| // PciDevices is a collection of every PCI device in |
| // /sys/bus/pci/devices . |
| // |
| // The map keys are the location of PCI devices. |
| type PciDevices map[string]PciDevice |
| |
| // PciDevices returns info for all PCI devices read from |
| // /sys/bus/pci/devices . |
| func (fs FS) PciDevices() (PciDevices, error) { |
| path := fs.sys.Path(pciDevicesPath) |
| |
| dirs, err := os.ReadDir(path) |
| if err != nil { |
| return nil, err |
| } |
| |
| pciDevs := make(PciDevices, len(dirs)) |
| for _, d := range dirs { |
| device, err := fs.parsePciDevice(d.Name()) |
| if err != nil { |
| return nil, err |
| } |
| |
| pciDevs[device.Name()] = *device |
| } |
| |
| return pciDevs, nil |
| } |
| |
| func parsePciDeviceLocation(loc string) (*PciDeviceLocation, error) { |
| locs := strings.Split(loc, ":") |
| if len(locs) != 3 { |
| return nil, fmt.Errorf("invalid location '%s'", loc) |
| } |
| locs = append(locs[0:2], strings.Split(locs[2], ".")...) |
| if len(locs) != 4 { |
| return nil, fmt.Errorf("invalid location '%s'", loc) |
| } |
| |
| seg, err := strconv.ParseInt(locs[0], 16, 32) |
| if err != nil { |
| return nil, fmt.Errorf("invalid segment: %w", err) |
| } |
| bus, err := strconv.ParseInt(locs[1], 16, 32) |
| if err != nil { |
| return nil, fmt.Errorf("invalid bus: %w", err) |
| } |
| device, err := strconv.ParseInt(locs[2], 16, 32) |
| if err != nil { |
| return nil, fmt.Errorf("invalid device: %w", err) |
| } |
| function, err := strconv.ParseInt(locs[3], 16, 32) |
| if err != nil { |
| return nil, fmt.Errorf("invalid function: %w", err) |
| } |
| |
| return &PciDeviceLocation{ |
| Segment: int(seg), |
| Bus: int(bus), |
| Device: int(device), |
| Function: int(function), |
| }, nil |
| } |
| |
| // Parse one PCI device |
| // Refer to https://docs.kernel.org/PCI/sysfs-pci.html |
| func (fs FS) parsePciDevice(name string) (*PciDevice, error) { |
| path := fs.sys.Path(pciDevicesPath, name) |
| // the file must be symbolic link. |
| realPath, err := os.Readlink(path) |
| if err != nil { |
| return nil, fmt.Errorf("failed to readlink: %w", err) |
| } |
| |
| // parse device location from realpath |
| // like "../../../devices/pci0000:00/0000:00:02.5/0000:04:00.0" |
| deviceLocStr := filepath.Base(realPath) |
| parentDeviceLocStr := filepath.Base(filepath.Dir(realPath)) |
| |
| deviceLoc, err := parsePciDeviceLocation(deviceLocStr) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse device location:%q %w", deviceLoc, err) |
| } |
| |
| // the parent device may have "pci" prefix. |
| // this is not pci device like bridges. |
| // we ignore such location to avoid confusion. |
| // TODO: is it really ok? |
| var parentDeviceLoc *PciDeviceLocation |
| if !strings.HasPrefix(parentDeviceLocStr, "pci") { |
| parentDeviceLoc, err = parsePciDeviceLocation(parentDeviceLocStr) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse parent device location %q: %w", parentDeviceLocStr, err) |
| } |
| } |
| |
| device := &PciDevice{ |
| Location: *deviceLoc, |
| ParentLocation: parentDeviceLoc, |
| } |
| |
| // These files must exist in a device directory. |
| for _, f := range [...]string{"class", "vendor", "device", "subsystem_vendor", "subsystem_device", "revision"} { |
| name := filepath.Join(path, f) |
| valueStr, err := util.SysReadFile(name) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read file %q: %w", name, err) |
| } |
| value, err := strconv.ParseInt(valueStr, 0, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse %s %q %s: %w", f, valueStr, device.Location, err) |
| } |
| |
| switch f { |
| case "class": |
| device.Class = uint32(value) |
| case "vendor": |
| device.Vendor = uint32(value) |
| case "device": |
| device.Device = uint32(value) |
| case "subsystem_vendor": |
| device.SubsystemVendor = uint32(value) |
| case "subsystem_device": |
| device.SubsystemDevice = uint32(value) |
| case "revision": |
| device.Revision = uint32(value) |
| default: |
| return nil, fmt.Errorf("unknown file %q", f) |
| } |
| } |
| |
| for _, f := range [...]string{"max_link_speed", "max_link_width", "current_link_speed", "current_link_width", "numa_node"} { |
| name := filepath.Join(path, f) |
| valueStr, err := util.SysReadFile(name) |
| if err != nil { |
| if os.IsNotExist(err) { |
| continue |
| } |
| return nil, fmt.Errorf("failed to read file %q: %w", name, err) |
| } |
| |
| // Some devices may be NULL or contain 'Unknown' as a value |
| // values defined in drivers/pci/probe.c pci_speed_string |
| if valueStr == "" || strings.HasPrefix(valueStr, "Unknown") { |
| continue |
| } |
| |
| switch f { |
| case "max_link_speed", "current_link_speed": |
| // example "8.0 GT/s PCIe" |
| values := strings.SplitAfterN(valueStr, " ", 2) |
| if len(values) != 2 { |
| return nil, fmt.Errorf("invalid value for %s %q %s", f, valueStr, device.Location) |
| } |
| if values[1] != "GT/s PCIe" { |
| return nil, fmt.Errorf("unknown unit for %s %q %s", f, valueStr, device.Location) |
| } |
| value, err := strconv.ParseFloat(strings.TrimSpace(values[0]), 64) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse %s %q %s: %w", f, valueStr, device.Location, err) |
| } |
| v := float64(value) |
| switch f { |
| case "max_link_speed": |
| device.MaxLinkSpeed = &v |
| case "current_link_speed": |
| device.CurrentLinkSpeed = &v |
| } |
| |
| case "max_link_width", "current_link_width": |
| value, err := strconv.ParseInt(valueStr, 10, 64) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse %s %q %s: %w", f, valueStr, device.Location, err) |
| } |
| v := float64(value) |
| switch f { |
| case "max_link_width": |
| device.MaxLinkWidth = &v |
| case "current_link_width": |
| device.CurrentLinkWidth = &v |
| } |
| |
| case "numa_node": |
| value, err := strconv.ParseInt(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse %s %q %s: %w", f, valueStr, device.Location, err) |
| } |
| v := int32(value) |
| device.NumaNode = &v |
| } |
| } |
| |
| // Parse SR-IOV files (these are optional and may not exist for all devices) |
| for _, f := range [...]string{"sriov_drivers_autoprobe", "sriov_numvfs", "sriov_offset", "sriov_stride", "sriov_totalvfs", "sriov_vf_device", "sriov_vf_total_msix"} { |
| name := filepath.Join(path, f) |
| valueStr, err := util.SysReadFile(name) |
| if err != nil { |
| if os.IsNotExist(err) { |
| continue // SR-IOV files are optional |
| } |
| return nil, fmt.Errorf("failed to read SR-IOV file %q %s: %w", name, device.Location, err) |
| } |
| |
| valueStr = strings.TrimSpace(valueStr) |
| if valueStr == "" { |
| continue |
| } |
| |
| switch f { |
| case "sriov_drivers_autoprobe": |
| // sriov_drivers_autoprobe is a boolean (0 or 1) |
| value, err := strconv.ParseInt(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV drivers autoprobe %q %s: %w", valueStr, device.Location, err) |
| } |
| v := value != 0 |
| device.SriovDriversAutoprobe = &v |
| |
| case "sriov_numvfs": |
| value, err := strconv.ParseUint(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV numvfs %q %s: %w", valueStr, device.Location, err) |
| } |
| v := uint32(value) |
| device.SriovNumvfs = &v |
| |
| case "sriov_offset": |
| value, err := strconv.ParseUint(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV offset %q %s: %w", valueStr, device.Location, err) |
| } |
| v := uint32(value) |
| device.SriovOffset = &v |
| |
| case "sriov_stride": |
| value, err := strconv.ParseUint(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV stride %q %s: %w", valueStr, device.Location, err) |
| } |
| v := uint32(value) |
| device.SriovStride = &v |
| |
| case "sriov_totalvfs": |
| value, err := strconv.ParseUint(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV totalvfs %q %s: %w", valueStr, device.Location, err) |
| } |
| v := uint32(value) |
| device.SriovTotalvfs = &v |
| |
| case "sriov_vf_device": |
| value, err := strconv.ParseUint(valueStr, 16, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV vf device %q %s: %w", valueStr, device.Location, err) |
| } |
| v := uint32(value) |
| device.SriovVfDevice = &v |
| |
| case "sriov_vf_total_msix": |
| value, err := strconv.ParseUint(valueStr, 10, 64) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse SR-IOV vf total msix %q %s: %w", valueStr, device.Location, err) |
| } |
| v := uint64(value) |
| device.SriovVfTotalMsix = &v |
| } |
| } |
| |
| // Parse power management files (these are optional and may not exist for all devices) |
| for _, f := range [...]string{"d3cold_allowed", "power_state"} { |
| name := filepath.Join(path, f) |
| valueStr, err := util.SysReadFile(name) |
| if err != nil { |
| if os.IsNotExist(err) { |
| continue // Power management files are optional |
| } |
| return nil, fmt.Errorf("failed to read power management file %q %s: %w", name, device.Location, err) |
| } |
| |
| valueStr = strings.TrimSpace(valueStr) |
| if valueStr == "" { |
| continue |
| } |
| |
| switch f { |
| case "d3cold_allowed": |
| // d3cold_allowed is a boolean (0 or 1) |
| value, err := strconv.ParseInt(valueStr, 10, 32) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse d3cold_allowed boolean %q %s: %w", valueStr, device.Location, err) |
| } |
| v := value != 0 |
| device.D3coldAllowed = &v |
| |
| case "power_state": |
| // power_state is a string (one of: "unknown", "error", "D0", "D1", "D2", "D3hot", "D3cold") |
| powerState := PciPowerState(valueStr) |
| device.PowerState = &powerState |
| } |
| } |
| |
| return device, nil |
| } |