Skip to content

Commit

Permalink
sriov: add support as special PCI devices
Browse files Browse the repository at this point in the history
Add support to report SRIOV devices.
Differently from GPU devices, we model SRIOV devices as special
PCI devices, extending the `pci` package instead of introducing
a new top-level package.
This design emerged during the review of a previous proposal:
9058f61#r755312597

SRIOV devices are either Physical Functions or Virtual functions.
The preferred representation for ghw is Physical Functions, whose
dependent devices will be Virtual Functions; however, for the sake of
practicality, the API also exposes soft references to Virtual Functions,
so consumers of the API can access them directly and not navigating
the parent devices.

This patch also adds support in `ghwc`, to report the sriov information,
and in the `snapshot` package, to make sure to capture all the files
in sysfs that ghw cares about.

Last but not least, lacking access to suitable non-linux systems,
support is provided only on linux OS, even though the API tries hard
not to be linux-specific.

Resolves: #92
Signed-off-by: Francesco Romani <fromani@redhat.com>
  • Loading branch information
ffromani committed Jul 21, 2023
1 parent 193a1aa commit 9701a46
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 4 deletions.
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.Functions {
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.

### 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
3 changes: 2 additions & 1 deletion 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
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

0 comments on commit 9701a46

Please sign in to comment.