Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose SRIOV information as PCI devices #315

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,39 @@ Subclass: VGA compatible controller [00]
Programming Interface: VGA controller [00]
```

#### SRIOV

SRIOV (Single-Root Input/Output Virtualization) is a class of PCI devices that ghw models explicitly.

```go
package main

import (
"fmt"

"github.com/jaypipes/ghw"
)

func main() {
pci, err := ghw.PCI()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting SRIOV info through PCI: %v", err)
}

fmt.Printf("%v\n", pci)

for _, dev := range pci.GetSRIOVDevices() {
fmt.Printf(" %v\n", dev)
}
}
```

`ghw` discovers the SRIOV devices by scanning PCI devices. Thus, you need to make sure to have scanned the PCI devices before
querying for SRIOV devices (aka "Functions", "PCI Functions").
Virtual Functions (VFs) are hosted on Physical Functions (PFs).
Virtual Functions are available both as entries in the `pci.Functions` slice and as properties of their parent Physical Functions.
Both references are aliases to the same object.
Comment on lines +912 to +913
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yuck, missed to update the rest of the docs :\


### GPU

The `ghw.GPU()` function returns a `ghw.GPUInfo` struct that contains
Expand Down
35 changes: 35 additions & 0 deletions cmd/ghwc/commands/sriov.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//

package commands

import (
"github.com/jaypipes/ghw"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

// sriovCmd represents the install command
var sriovCmd = &cobra.Command{
Use: "sriov",
Short: "Show Single Root I/O Virtualization device information for the host system",
RunE: showSRIOV,
}

// showSRIOV shows SRIOV information for the host system.
func showSRIOV(cmd *cobra.Command, args []string) error {
info, err := ghw.PCI()
if err != nil {
return errors.Wrap(err, "error getting SRIOV info through PCI")
}

printInfo(info.DescribeDevices(info.GetSRIOVDevices()))
return nil
}

func init() {
rootCmd.AddCommand(sriovCmd)
}
35 changes: 35 additions & 0 deletions pkg/pci/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//

package pci

import "fmt"

// Function describes an SR-IOV physical or virtual function. Physical functions
// will have no Parent Function struct pointer and will have one or more Function
// structs in the Virtual field.
type Function struct {
Parent *Function `json:"parent,omitempty"`
// MaxVirtual contains the maximum number of supported virtual
// functions for this physical function
MaxVirtual int `json:"max_virtual,omitempty"`
// Virtual contains the physical function's virtual functions
Virtual []*Function `json:"virtual_functions"`
}

// IsPhysical returns true if the PCIe function is a physical function, false
// if it is a virtual function. It is safe to assume that if a function is not
// physical, then is virtual (e.g. can't be anything else)
func (f *Function) IsPhysical() bool {
return f.Parent == nil
}

func (f *Function) String() string {
if f.IsPhysical() {
return fmt.Sprintf("function: 'physical' virtual: '%d/%d'", len(f.Virtual), f.MaxVirtual)
}
return "function: 'virtual'"
}
105 changes: 105 additions & 0 deletions pkg/pci/function_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//

package pci

import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/jaypipes/ghw/pkg/linuxpath"
"github.com/jaypipes/ghw/pkg/util"
)

// GetSRIOVDevices returns only the PCI devices that are
// Single Root I/O Virtualization (SR-IOV) capable -- either
// physical of virtual functions.
func (i *Info) GetSRIOVDevices() []*Device {
res := []*Device{}
for _, dev := range i.Devices {
if dev.Function == nil {
continue
}
res = append(res, dev)
}
return res
}

func (info *Info) fillSRIOVDevices() error {
for _, dev := range info.Devices {
isPF, err := info.fillPhysicalFunctionForDevice(dev)
if !isPF {
// not a physical function, nothing to do
continue
}
if err != nil {
return err
}
}
return nil
}

func (info *Info) fillPhysicalFunctionForDevice(dev *Device) (bool, error) {
paths := linuxpath.New(info.ctx)
devPath := filepath.Join(paths.SysBusPciDevices, dev.Address)

buf, err := os.ReadFile(filepath.Join(devPath, "sriov_totalvfs"))
if err != nil {
// is not a physfn. Since we will fill virtfn from physfn, we can give up now
// note we intentionally swallow the error.
return false, nil
}

maxVFs, err := strconv.Atoi(strings.TrimSpace(string(buf)))
if err != nil {
return true, fmt.Errorf("cannot reading sriov_totalvfn: %w", err)
}

pf := &Function{
MaxVirtual: maxVFs,
}
err = info.fillVirtualFunctionsForPhysicalFunction(pf, devPath)
if err != nil {
return true, fmt.Errorf("cannot inspect VFs: %w", err)
}
dev.Function = pf
return true, nil
}

func (info *Info) fillVirtualFunctionsForPhysicalFunction(parentFn *Function, parentPath string) error {
numVfs := util.SafeIntFromFile(info.ctx, filepath.Join(parentPath, "sriov_numvfs"))
if numVfs == -1 {
return fmt.Errorf("invalid number of virtual functions: %v", numVfs)
}

var vfs []*Function
for vfnIdx := 0; vfnIdx < numVfs; vfnIdx++ {
virtFn := fmt.Sprintf("virtfn%d", vfnIdx)
vfnDest, err := os.Readlink(filepath.Join(parentPath, virtFn))
if err != nil {
return fmt.Errorf("error reading backing device for virtfn %q: %w", virtFn, err)
}

vfnAddr := filepath.Base(vfnDest)
vfnDev := info.GetDevice(vfnAddr)
if vfnDev == nil {
return fmt.Errorf("error finding the PCI device for virtfn %s", vfnAddr)
}

// functions must be ordered by their index
vf := &Function{
Parent: parentFn,
}

vfs = append(vfs, vf)
vfnDev.Function = vf
}

parentFn.Virtual = vfs
return nil
}
13 changes: 13 additions & 0 deletions pkg/pci/function_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !linux
// +build !linux

// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//

package pci

func (i *Info) GetSRIOVDevices() []*Device {
return nil
}
46 changes: 45 additions & 1 deletion pkg/pci/pci.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package pci
import (
"encoding/json"
"fmt"
"strings"

"github.com/jaypipes/pcidb"

Expand Down Expand Up @@ -36,6 +37,8 @@ type Device struct {
// architecture is not NUMA.
Node *topology.Node `json:"node,omitempty"`
Driver string `json:"driver"`
// If this device is a SRIOV function, this field is non-nil
Function *Function `json:"function,omitempty"`
}

type devIdent struct {
Expand Down Expand Up @@ -105,13 +108,18 @@ func (d *Device) String() string {
if d.Class != nil {
className = d.Class.Name
}
fnInfo := ""
if d.Function != nil {
fnInfo = " " + d.Function.String()
}
return fmt.Sprintf(
"%s -> driver: '%s' class: '%s' vendor: '%s' product: '%s'",
"%s -> driver: '%s' class: '%s' vendor: '%s' product: '%s'%s",
d.Address,
d.Driver,
className,
vendorName,
productName,
fnInfo,
)
}

Expand All @@ -127,6 +135,42 @@ func (i *Info) String() string {
return fmt.Sprintf("PCI (%d devices)", len(i.Devices))
}

type DevicesPrinter struct {
ctx *context.Context
devs []*Device
}

func (dp DevicesPrinter) String() string {
var sb strings.Builder
for _, dev := range dp.devs {
fmt.Fprintf(&sb, "%s\n", dev.String())
}
return sb.String()
}

func (dp DevicesPrinter) JSONString(pretty bool) string {
var sb strings.Builder
for _, dev := range dp.devs {
fmt.Fprintf(&sb, "%s\n", marshal.SafeJSON(dp.ctx, dev, pretty))
}
return sb.String()
}

func (dp DevicesPrinter) YAMLString() string {
var sb strings.Builder
for _, dev := range dp.devs {
fmt.Fprintf(&sb, "%s\n", marshal.SafeYAML(dp.ctx, dev))
}
return sb.String()
}

func (i *Info) DescribeDevices(devs []*Device) DevicesPrinter {
return DevicesPrinter{
ctx: i.ctx,
devs: devs,
}
}

// New returns a pointer to an Info struct that contains information about the
// PCI devices on the host system
func New(opts ...*option.Option) (*Info, error) {
Expand Down
7 changes: 4 additions & 3 deletions pkg/pci/pci_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ func (i *Info) load() error {
}
i.db = db
i.Devices = i.getDevices()
return nil
// we need to do another pass once we filled all the PCI devices.
return i.fillSRIOVDevices()
}

func getDeviceModaliasPath(ctx *context.Context, pciAddr *pciaddr.Address) string {
Expand Down Expand Up @@ -404,9 +405,9 @@ func (info *Info) getDevices() []*Device {
dev = info.GetDevice(addr)
if dev == nil {
info.ctx.Warn("failed to get device information for PCI address %s", addr)
} else {
devs = append(devs, dev)
continue
}
devs = append(devs, dev)
}
return devs
}
2 changes: 1 addition & 1 deletion pkg/pci/pci_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

func (i *Info) load() error {
return errors.New("pciFillInfo not implemented on " + runtime.GOOS)
return errors.New("pci load() not implemented on " + runtime.GOOS)
}

// GetDevice returns a pointer to a Device struct that describes the PCI
Expand Down
15 changes: 14 additions & 1 deletion pkg/snapshot/clonetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func CopyFilesInto(fileSpecs []string, destDir string, opts *CopyFileOptions) er
if opts == nil {
opts = &CopyFileOptions{
IsSymlinkFn: isSymlink,
ShouldCreateDirFn: isDriversDir,
ShouldCreateDirFn: shouldCreateDir,
}
}
for _, fileSpec := range fileSpecs {
Expand Down Expand Up @@ -156,6 +156,13 @@ func copyFileTreeInto(paths []string, destDir string, opts *CopyFileOptions) err
return nil
}

func shouldCreateDir(path string, fi os.FileInfo) bool {
if isDeviceNetworkDir(path, fi) {
return true
}
return isDriversDir(path, fi)
}

func isSymlink(path string, fi os.FileInfo) bool {
return fi.Mode()&os.ModeSymlink != 0
}
Expand All @@ -164,6 +171,12 @@ func isDriversDir(path string, fi os.FileInfo) bool {
return strings.Contains(path, "drivers")
}

func isDeviceNetworkDir(path string, fi os.FileInfo) bool {
parentDir := filepath.Base(filepath.Dir(path))
// TODO: the "HasPrefix" check is brutal, but should work on linux
return parentDir == "net" && strings.HasPrefix(path, "/sys/devices")
}

func copyLink(path, targetPath string) error {
target, err := os.Readlink(path)
if err != nil {
Expand Down
Loading