Skip to content

Commit

Permalink
WIP: RFC: expose SRIOV information
Browse files Browse the repository at this point in the history
WIP patch to demonstrate the possible API

TBD: add tests
TBD: provide rationale in the commit message

Signed-off-by: Francesco Romani <fromani@redhat.com>
  • Loading branch information
ffromani committed Mar 18, 2021
1 parent 1a1bc99 commit bbe0fb9
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 18 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,40 @@ information
`ghw.TopologyNode` struct if you'd like to dig deeper into the NUMA/topology
subsystem

### SRIOV

*This API is PROVISIONAL! ghw will try hard to not make breaking changes to this API, but still users are advice this new API is not
declared stable yet. We expect to declare it stable with ghw version 1.0.0*

SRIOV (Single-Root Input/Output Virtualization) is a class of PCI devices that ghw models explicitly, like gpus.
TODO: add more text?


```go
package main

import (
"fmt"

"github.com/jaypipes/ghw"
)

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

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

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

If the function returns `nil`, then the device is not recognized as a SRIOV device.

### Chassis

The host's chassis information is accessible with the `ghw.Chassis()` function. This
Expand Down
9 changes: 9 additions & 0 deletions alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/jaypipes/ghw/pkg/pci"
pciaddress "github.com/jaypipes/ghw/pkg/pci/address"
"github.com/jaypipes/ghw/pkg/product"
"github.com/jaypipes/ghw/pkg/sriov"
"github.com/jaypipes/ghw/pkg/topology"
)

Expand Down Expand Up @@ -146,3 +147,11 @@ type GraphicsCard = gpu.GraphicsCard
var (
GPU = gpu.New
)

type SRIOVInfo = sriov.Info
type PhysicalFunction = sriov.PhysicalFunction
type VirtualFunction = sriov.VirtualFunction

var (
SRIOV = sriov.New
)
48 changes: 48 additions & 0 deletions cmd/ghwc/commands/sriov.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// 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 (
"fmt"

"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 SRIOV devices information for the host system",
RunE: showSRIOV,
}

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

switch outputFormat {
case outputFormatHuman:
fmt.Printf("%v\n", sriov)

for _, dev := range sriov.PhysicalFunctions {
fmt.Printf(" %v\n", dev)
}
case outputFormatJSON:
fmt.Printf("%s\n", sriov.JSONString(pretty))
case outputFormatYAML:
fmt.Printf("%s", sriov.YAMLString())
}
return nil
}

func init() {
rootCmd.AddCommand(sriovCmd)
}
40 changes: 26 additions & 14 deletions host.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package ghw

import (
"fmt"
"strings"

"github.com/jaypipes/ghw/pkg/context"

Expand All @@ -22,6 +23,7 @@ import (
"github.com/jaypipes/ghw/pkg/net"
"github.com/jaypipes/ghw/pkg/pci"
"github.com/jaypipes/ghw/pkg/product"
"github.com/jaypipes/ghw/pkg/sriov"
"github.com/jaypipes/ghw/pkg/topology"
)

Expand All @@ -40,6 +42,7 @@ type HostInfo struct {
Baseboard *baseboard.Info `json:"baseboard"`
Product *product.Info `json:"product"`
PCI *pci.Info `json:"pci"`
SRIOV *sriov.Info `json:"sriov"`
}

// Host returns a pointer to a HostInfo struct that contains fields with
Expand Down Expand Up @@ -91,6 +94,10 @@ func Host(opts ...*WithOption) (*HostInfo, error) {
if err != nil {
return nil, err
}
sriovInfo, err := sriov.New(opts...)
if err != nil {
return nil, err
}
return &HostInfo{
ctx: ctx,
CPU: cpuInfo,
Expand All @@ -104,26 +111,31 @@ func Host(opts ...*WithOption) (*HostInfo, error) {
Baseboard: baseboardInfo,
Product: productInfo,
PCI: pciInfo,
SRIOV: sriovInfo,
}, nil
}

// String returns a newline-separated output of the HostInfo's component
// structs' String-ified output
func (info *HostInfo) String() string {
return fmt.Sprintf(
"%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
info.Block.String(),
info.CPU.String(),
info.GPU.String(),
info.Memory.String(),
info.Network.String(),
info.Topology.String(),
info.Chassis.String(),
info.BIOS.String(),
info.Baseboard.String(),
info.Product.String(),
info.PCI.String(),
)
var b strings.Builder
for _, s := range []fmt.Stringer{
info.Block,
info.CPU,
info.GPU,
info.Memory,
info.Network,
info.Topology,
info.Chassis,
info.BIOS,
info.Baseboard,
info.Product,
info.PCI,
info.SRIOV,
} {
b.WriteString(s.String())
}
return b.String()
}

// YAMLString returns a string with the host information formatted as YAML
Expand Down
14 changes: 10 additions & 4 deletions pkg/pci/pci.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@ func (i *Info) String() string {
return fmt.Sprintf("PCI (%d devices)", len(i.Devices))
}

// 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) {
ctx := context.New(opts...)
// NewWithContext returns a pointer to an Info struct that contains information about
// the PCI devices on the host system. Use this function when you want to consume
// the topology package from another package (e.g. sriov)
func NewWithContext(ctx *context.Context) (*Info, error) {
// by default we don't report NUMA information;
// we will only if are sure we are running on NUMA architecture
arch := topology.ARCHITECTURE_SMP
Expand All @@ -171,6 +171,12 @@ func New(opts ...*option.Option) (*Info, error) {
return info, nil
}

// 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) {
return NewWithContext(context.New(opts...))
}

// simple private struct used to encapsulate PCI information in a top-level
// "pci" YAML/JSON map/object key
type pciPrinter struct {
Expand Down
6 changes: 6 additions & 0 deletions pkg/snapshot/clonetree.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package snapshot

import (
"errors"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -197,6 +198,11 @@ func copyLink(path, targetPath string) error {
return err
}
if err := os.Symlink(target, targetPath); err != nil {
// multiple sources can link back to the same entry: a notable example is
// all the SRIOV VFs linking back to the same driver.
if errors.Is(err, os.ErrExist) {
return nil
}
return err
}

Expand Down
17 changes: 17 additions & 0 deletions pkg/snapshot/clonetree_pci.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ func scanPCIDeviceRoot(root string) (fileSpecs []string, pciRoots []string) {
"revision",
"vendor",
}

perDevEntriesOpt := []string{
"driver",
"net/*",
"physfn",
"sriov_*",
"virtfn*",
}

entries, err := ioutil.ReadDir(root)
if err != nil {
return []string{}, []string{}
Expand All @@ -93,6 +102,14 @@ func scanPCIDeviceRoot(root string) (fileSpecs []string, pciRoots []string) {
fileSpecs = append(fileSpecs, filepath.Join(pciEntry, perNetEntry))
}

for _, perNetEntryOpt := range perDevEntriesOpt {
netEntryOptPath := filepath.Join(pciEntry, perNetEntryOpt)
if items, err := filepath.Glob(netEntryOptPath); err == nil && len(items) > 0 {
fileSpecs = append(fileSpecs, netEntryOptPath)
}

}

if isPCIBridge(entryPath) {
trace("adding new PCI root %q\n", entryName)
pciRoots = append(pciRoots, pciEntry)
Expand Down
117 changes: 117 additions & 0 deletions pkg/sriov/sriov.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//

package sriov

import (
"fmt"

"github.com/jaypipes/ghw/pkg/context"
"github.com/jaypipes/ghw/pkg/marshal"
"github.com/jaypipes/ghw/pkg/option"
"github.com/jaypipes/ghw/pkg/pci"
pciaddr "github.com/jaypipes/ghw/pkg/pci/address"
)

type Device struct {
Driver string `json:"driver"`
Interfaces []string `json:"interfaces"`
// the PCI address where the SRIOV instance can be found
Address *pciaddr.Address `json:"address"`
PCI *pci.Device `json:"pci"`
}

type PhysicalFunction struct {
Device
MaxVFNum int `json:"max_vf_num,omitempty"`
VFs []VirtualFunction `json:"vfs,omitempty"`
}

type VirtualFunction struct {
Device
ID int `json:"id"`
// Address of the (parent) Physical Function this Virtual Function pertains to.
ParentAddress *pciaddr.Address `json:"parent_address,omitempty"`
}

func (pf *PhysicalFunction) String() string {
deviceStr := pf.Address.String()
nodeStr := ""
if pf.PCI != nil {
deviceStr = pf.PCI.String()
if pf.PCI.Node != nil {
nodeStr = fmt.Sprintf(" [affined to NUMA node %d]", pf.PCI.Node.ID)
}
}
return fmt.Sprintf(
"physical function %s@%s with %d/%d virtual functions",
nodeStr,
deviceStr,
len(pf.VFs),
pf.MaxVFNum,
)
}

func (vf *VirtualFunction) String() string {
deviceStr := vf.Address.String()
nodeStr := ""
if vf.PCI != nil {
deviceStr = vf.PCI.String()
if vf.PCI.Node != nil {
nodeStr = fmt.Sprintf(" [affined to NUMA node %d]", vf.PCI.Node.ID)
}
}
return fmt.Sprintf(
"virtual function %d %s@%s from %s",
vf.ID,
nodeStr,
deviceStr,
vf.ParentAddress,
)
}

type Info struct {
ctx *context.Context
PhysicalFunctions []*PhysicalFunction `json:"physical_functions,omitempty"`
VirtualFunctions []*VirtualFunction `json:"virtual_functions,omitempty"`
}

// New returns a pointer to an Info struct that contains information about the
// SRIOV devices on the host system
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
}
return info, nil
}

func (i *Info) String() string {
return fmt.Sprintf(
"sriov (%d phsyical %d virtual devices)",
len(i.PhysicalFunctions),
len(i.VirtualFunctions),
)
}

// simple private struct used to encapsulate gpu information in a top-level
// "sriov" YAML/JSON map/object key
type sriovPrinter struct {
Info *Info `json:"sriov,omitempty"`
}

// YAMLString returns a string with the gpu information formatted as YAML
// under a top-level "gpu:" key
func (i *Info) YAMLString() string {
return marshal.SafeYAML(i.ctx, sriovPrinter{i})
}

// JSONString returns a string with the gpu information formatted as JSON
// under a top-level "gpu:" key
func (i *Info) JSONString(indent bool) string {
return marshal.SafeJSON(i.ctx, sriovPrinter{i}, indent)
}
Loading

0 comments on commit bbe0fb9

Please sign in to comment.