619 lines
16 KiB
Go
619 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
ethtoolGRXRINGS = 0x2d
|
|
ethtoolGRXCLSRLCNT = 0x2e
|
|
ethtoolGRXCLSRULE = 0x2f
|
|
ethtoolGRXCLSRLALL = 0x30
|
|
ethtoolSRXCLSRLDEL = 0x31
|
|
ethtoolSRXCLSRLINS = 0x32
|
|
etherFlow = 0x12
|
|
)
|
|
|
|
type ethtoolFlowExt struct {
|
|
padding [2]byte
|
|
hDest [6]byte
|
|
vlanEtype uint16
|
|
vlanTci uint16
|
|
data [2]uint32
|
|
}
|
|
|
|
type ethtoolRxFlowSpec struct {
|
|
flowType uint32
|
|
hU [52]byte
|
|
hExt ethtoolFlowExt
|
|
mU [52]byte
|
|
mExt ethtoolFlowExt
|
|
_ [4]byte
|
|
ringCookie uint64
|
|
location uint32
|
|
_ [4]byte
|
|
}
|
|
|
|
type ethtoolRxnfc struct {
|
|
cmd uint32
|
|
flowType uint32
|
|
data uint64
|
|
fs ethtoolRxFlowSpec
|
|
ruleCnt uint32
|
|
_ [4]byte
|
|
}
|
|
|
|
func rxRings(fd int, ifname string) (uint64, error) {
|
|
nfc := ethtoolRxnfc{cmd: ethtoolGRXRINGS}
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&nfc)); err != nil {
|
|
return 0, err
|
|
}
|
|
return nfc.data, nil
|
|
}
|
|
|
|
// Returns the installed rule locations and the total filter capacity. ice
|
|
// only honours filters near the top of that range, which is why ethtool's own
|
|
// rule manager allocates downwards from the end.
|
|
func allRuleLocations(fd int, ifname string) ([]uint32, uint32, error) {
|
|
cnt := ethtoolRxnfc{cmd: ethtoolGRXCLSRLCNT}
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&cnt)); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
capacity := uint32(cnt.data)
|
|
if cnt.ruleCnt == 0 {
|
|
return nil, capacity, nil
|
|
}
|
|
|
|
// rule_locs follows rule_cnt directly, before the struct's tail padding,
|
|
// and that is where the kernel copies it to.
|
|
locOff := unsafe.Offsetof(ethtoolRxnfc{}.ruleCnt) + 4
|
|
buf := make([]byte, int(unsafe.Sizeof(ethtoolRxnfc{}))+4*int(cnt.ruleCnt))
|
|
all := (*ethtoolRxnfc)(unsafe.Pointer(&buf[0]))
|
|
all.cmd = ethtoolGRXCLSRLALL
|
|
all.ruleCnt = cnt.ruleCnt
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&buf[0])); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if all.ruleCnt > cnt.ruleCnt {
|
|
return nil, 0, fmt.Errorf("%s reported %d filter locations into room for %d",
|
|
ifname, all.ruleCnt, cnt.ruleCnt)
|
|
}
|
|
|
|
raw := buf[locOff:]
|
|
locs := make([]uint32, 0, all.ruleCnt)
|
|
for i := 0; i < int(all.ruleCnt); i++ {
|
|
locs = append(locs, binary.LittleEndian.Uint32(raw[i*4:]))
|
|
}
|
|
return locs, capacity, nil
|
|
}
|
|
|
|
func ruleIsEther(fd int, ifname string, loc uint32) bool {
|
|
get := ethtoolRxnfc{cmd: ethtoolGRXCLSRULE}
|
|
get.fs.location = loc
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&get)); err != nil {
|
|
return false
|
|
}
|
|
return get.fs.flowType&0xff == etherFlow
|
|
}
|
|
|
|
func deleteRule(fd int, ifname string, loc uint32) error {
|
|
nfc := ethtoolRxnfc{cmd: ethtoolSRXCLSRLDEL}
|
|
nfc.fs.location = loc
|
|
return ethtoolCall(fd, ifname, unsafe.Pointer(&nfc))
|
|
}
|
|
|
|
// ice rejects RX_CLS_LOC_ANY, so the caller must supply a free location.
|
|
func insertEtherRule(fd int, ifname string, ethType uint16, queue uint64, loc uint32) error {
|
|
nfc := ethtoolRxnfc{cmd: ethtoolSRXCLSRLINS}
|
|
nfc.fs.flowType = etherFlow
|
|
binary.BigEndian.PutUint16(nfc.fs.hU[12:14], ethType)
|
|
// A set mask bit means that bit must match, so mask only the ethertype and
|
|
// leave both MAC masks zero. Note ethtool -n prints the complement of this.
|
|
binary.BigEndian.PutUint16(nfc.fs.mU[12:14], 0xffff)
|
|
nfc.fs.ringCookie = queue
|
|
nfc.fs.location = loc
|
|
return ethtoolCall(fd, ifname, unsafe.Pointer(&nfc))
|
|
}
|
|
|
|
// Our rules survive process exit, so stale ones are cleared before the queue
|
|
// configuration is touched and fresh ones installed.
|
|
func clearFlowRules(fd int, ifname string) checkResult {
|
|
res := checkResult{item: ifname + " stale rules"}
|
|
locs, _, err := allRuleLocations(fd, ifname)
|
|
if err != nil {
|
|
res.err = err
|
|
return res
|
|
}
|
|
n := 0
|
|
for _, loc := range locs {
|
|
if !ruleIsEther(fd, ifname, loc) {
|
|
continue
|
|
}
|
|
if err := deleteRule(fd, ifname, loc); err != nil {
|
|
res.err = fmt.Errorf("deleting rule %d: %w", loc, err)
|
|
return res
|
|
}
|
|
n++
|
|
}
|
|
res.fixed = n > 0
|
|
res.state = fmt.Sprintf("%d removed", n)
|
|
return res
|
|
}
|
|
|
|
func checkFlowRules(fd int, ifname string, ethertypes []uint16) checkResult {
|
|
res := checkResult{item: ifname + " flow rules"}
|
|
rings, err := rxRings(fd, ifname)
|
|
if err != nil {
|
|
res.err = err
|
|
return res
|
|
}
|
|
if uint64(len(ethertypes)) > rings {
|
|
res.err = fmt.Errorf("%d streams needs %d rx rings, only %d available",
|
|
len(ethertypes), len(ethertypes), rings)
|
|
return res
|
|
}
|
|
|
|
locs, capacity, err := allRuleLocations(fd, ifname)
|
|
if err != nil {
|
|
res.err = err
|
|
return res
|
|
}
|
|
if capacity < uint32(len(ethertypes)) {
|
|
res.err = fmt.Errorf("filter capacity %d is below %d streams", capacity, len(ethertypes))
|
|
return res
|
|
}
|
|
taken := make(map[uint32]bool, len(locs))
|
|
for _, loc := range locs {
|
|
taken[loc] = true
|
|
}
|
|
|
|
// Inserting at a taken location would evict it, and would then leave every
|
|
// later ethertype evicting the one before it at that same location.
|
|
next := capacity - 1
|
|
for i, et := range ethertypes {
|
|
for taken[next] {
|
|
if next == 0 {
|
|
res.err = fmt.Errorf("no free filter location below %d for ethertype 0x%04x",
|
|
capacity, et)
|
|
return res
|
|
}
|
|
next--
|
|
}
|
|
if err := insertEtherRule(fd, ifname, et, uint64(i), next); err != nil {
|
|
res.err = fmt.Errorf("steering ethertype 0x%04x to queue %d at location %d: %w",
|
|
et, i, next, err)
|
|
return res
|
|
}
|
|
taken[next] = true
|
|
}
|
|
|
|
res.state = fmt.Sprintf("0x%04x-0x%04x to queues 0-%d of %d",
|
|
ethertypes[0], ethertypes[len(ethertypes)-1], len(ethertypes)-1, rings)
|
|
return res
|
|
}
|
|
|
|
// The ifreq shape used by every ioctl that passes its payload by pointer.
|
|
type dataIfreq struct {
|
|
name [unix.IFNAMSIZ]byte
|
|
data unsafe.Pointer
|
|
_ [16]byte
|
|
}
|
|
|
|
type flagsIfreq struct {
|
|
name [unix.IFNAMSIZ]byte
|
|
flags uint16
|
|
_ [22]byte
|
|
}
|
|
|
|
type ethtoolRingparam struct {
|
|
cmd uint32
|
|
rxMaxPending uint32
|
|
rxMiniMaxPending uint32
|
|
rxJumboMaxPending uint32
|
|
txMaxPending uint32
|
|
rxPending uint32
|
|
rxMiniPending uint32
|
|
rxJumboPending uint32
|
|
txPending uint32
|
|
}
|
|
|
|
type ethtoolCoalesce struct {
|
|
cmd uint32
|
|
rxCoalesceUsecs uint32
|
|
rxMaxCoalescedFrames uint32
|
|
rxCoalesceUsecsIrq uint32
|
|
rxMaxCoalescedFramesIrq uint32
|
|
txCoalesceUsecs uint32
|
|
txMaxCoalescedFrames uint32
|
|
txCoalesceUsecsIrq uint32
|
|
txMaxCoalescedFramesIrq uint32
|
|
statsBlockCoalesceUsecs uint32
|
|
useAdaptiveRxCoalesce uint32
|
|
useAdaptiveTxCoalesce uint32
|
|
pktRateLow uint32
|
|
rxCoalesceUsecsLow uint32
|
|
rxMaxCoalescedFramesLow uint32
|
|
txCoalesceUsecsLow uint32
|
|
txMaxCoalescedFramesLow uint32
|
|
pktRateHigh uint32
|
|
rxCoalesceUsecsHigh uint32
|
|
rxMaxCoalescedFramesHigh uint32
|
|
txCoalesceUsecsHigh uint32
|
|
txMaxCoalescedFramesHigh uint32
|
|
rateSampleInterval uint32
|
|
}
|
|
|
|
const (
|
|
wantGovernor = "performance"
|
|
wantCoalesceUsecs = 25
|
|
wantRxRing = 8160
|
|
wantTxRing = 4096
|
|
)
|
|
|
|
type checkResult struct {
|
|
item string
|
|
state string
|
|
fixed bool
|
|
err error
|
|
}
|
|
|
|
func (r checkResult) status() string {
|
|
switch {
|
|
case r.err != nil:
|
|
return paint("FAIL", cRed)
|
|
case r.fixed:
|
|
return paint("FIXED", cYellow)
|
|
default:
|
|
return paint("ok", cGreen)
|
|
}
|
|
}
|
|
|
|
func (r checkResult) detail() string {
|
|
if r.err != nil {
|
|
if r.state == "" {
|
|
return r.err.Error()
|
|
}
|
|
return fmt.Sprintf("%s: %v", r.state, r.err)
|
|
}
|
|
return r.state
|
|
}
|
|
|
|
func reportChecks(title string, results []checkResult) error {
|
|
var rows [][]string
|
|
var failed []string
|
|
for _, r := range results {
|
|
rows = append(rows, []string{r.item, r.status(), r.detail()})
|
|
if r.err != nil {
|
|
failed = append(failed, r.item)
|
|
}
|
|
}
|
|
fmt.Println(renderBox(title,
|
|
[]string{"CHECK", "STATUS", "DETAIL"},
|
|
[]bool{false, false, false}, rows))
|
|
if len(failed) > 0 {
|
|
return fmt.Errorf("cannot test with %s in this state", strings.Join(failed, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ethtoolCall(fd int, ifname string, data unsafe.Pointer) error {
|
|
var ifr dataIfreq
|
|
if len(ifname) >= unix.IFNAMSIZ {
|
|
return fmt.Errorf("interface name %q too long", ifname)
|
|
}
|
|
copy(ifr.name[:], ifname)
|
|
ifr.data = data
|
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
|
uintptr(unix.SIOCETHTOOL), uintptr(unsafe.Pointer(&ifr)))
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getRings(fd int, ifname string) (ethtoolRingparam, error) {
|
|
rp := ethtoolRingparam{cmd: unix.ETHTOOL_GRINGPARAM}
|
|
err := ethtoolCall(fd, ifname, unsafe.Pointer(&rp))
|
|
return rp, err
|
|
}
|
|
|
|
func getCoalesce(fd int, ifname string) (ethtoolCoalesce, error) {
|
|
ec := ethtoolCoalesce{cmd: unix.ETHTOOL_GCOALESCE}
|
|
err := ethtoolCall(fd, ifname, unsafe.Pointer(&ec))
|
|
return ec, err
|
|
}
|
|
|
|
const (
|
|
ethSSStats = 1
|
|
ethGstringLen = 32
|
|
)
|
|
|
|
// Each of these has a __u32 or __u8 tail the kernel fills past the end of the
|
|
// struct, so they are only ever the header on a larger buffer.
|
|
type ethtoolSsetInfo struct {
|
|
cmd uint32
|
|
reserved uint32
|
|
ssetMask uint64
|
|
data [1]uint32
|
|
_ [4]byte
|
|
}
|
|
|
|
type ethtoolGstrings struct {
|
|
cmd uint32
|
|
stringSet uint32
|
|
len uint32
|
|
}
|
|
|
|
type ethtoolStatsHdr struct {
|
|
cmd uint32
|
|
nStats uint32
|
|
}
|
|
|
|
// The stats payload is an array of __u64 immediately after the header, so the
|
|
// buffer is allocated as uint64 to guarantee it lands on an eight byte
|
|
// boundary. A []byte carries no such guarantee.
|
|
func statsBuf(n int) []uint64 { return make([]uint64, n) }
|
|
|
|
func statCount(fd int, ifname string) (uint32, error) {
|
|
req := ethtoolSsetInfo{cmd: unix.ETHTOOL_GSSET_INFO, ssetMask: 1 << ethSSStats}
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&req)); err != nil {
|
|
return 0, err
|
|
}
|
|
// A cleared mask bit means the driver does not have that string set at all,
|
|
// in which case no count was written into the tail.
|
|
if req.ssetMask == 0 {
|
|
return 0, fmt.Errorf("driver has no ETH_SS_STATS string set")
|
|
}
|
|
return req.data[0], nil
|
|
}
|
|
|
|
func statNames(fd int, ifname string) ([]string, error) {
|
|
n, err := statCount(fd, ifname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n == 0 {
|
|
return nil, fmt.Errorf("driver reports zero statistics")
|
|
}
|
|
|
|
hdrLen := int(unsafe.Sizeof(ethtoolGstrings{}))
|
|
buf := statsBuf((hdrLen + int(n)*ethGstringLen + 7) / 8)
|
|
hdr := (*ethtoolGstrings)(unsafe.Pointer(&buf[0]))
|
|
hdr.cmd = unix.ETHTOOL_GSTRINGS
|
|
hdr.stringSet = ethSSStats
|
|
hdr.len = n
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&buf[0])); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
raw := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8)[hdrLen:]
|
|
names := make([]string, hdr.len)
|
|
for i := range names {
|
|
s := raw[i*ethGstringLen : (i+1)*ethGstringLen]
|
|
if k := bytes.IndexByte(s, 0); k >= 0 {
|
|
s = s[:k]
|
|
}
|
|
names[i] = string(s)
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
func statValues(fd int, ifname string, n uint32, into []uint64) error {
|
|
hdr := (*ethtoolStatsHdr)(unsafe.Pointer(&into[0]))
|
|
hdr.cmd = unix.ETHTOOL_GSTATS
|
|
hdr.nStats = n
|
|
return ethtoolCall(fd, ifname, unsafe.Pointer(&into[0]))
|
|
}
|
|
|
|
// The ioctl indexes counters by position, and the ordering is a property of the
|
|
// driver build, so names are resolved to indices once rather than per poll.
|
|
type statReader struct {
|
|
fd int
|
|
ifname string
|
|
n uint32
|
|
want []int
|
|
buf []uint64
|
|
}
|
|
|
|
func newStatReader(fd int, ifname string, want []string) (*statReader, error) {
|
|
names, err := statNames(fd, ifname)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s statistics: %w", ifname, err)
|
|
}
|
|
idx := make(map[string]int, len(names))
|
|
for i, s := range names {
|
|
idx[s] = i
|
|
}
|
|
r := &statReader{
|
|
fd: fd,
|
|
ifname: ifname,
|
|
n: uint32(len(names)),
|
|
buf: statsBuf(1 + len(names)),
|
|
}
|
|
for _, w := range want {
|
|
i, ok := idx[w]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s has no statistic %q", ifname, w)
|
|
}
|
|
r.want = append(r.want, i)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (r *statReader) sum() uint64 {
|
|
if err := statValues(r.fd, r.ifname, r.n, r.buf); err != nil {
|
|
panic(fmt.Sprintf("reading %s statistics: %v", r.ifname, err))
|
|
}
|
|
vals := r.buf[1:]
|
|
var total uint64
|
|
for _, i := range r.want {
|
|
total += vals[i]
|
|
}
|
|
return total
|
|
}
|
|
|
|
func checkGovernor(want string) checkResult {
|
|
res := checkResult{item: "cpu governor"}
|
|
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor")
|
|
if err != nil || len(paths) == 0 {
|
|
res.err = fmt.Errorf("no cpufreq governors found")
|
|
return res
|
|
}
|
|
var wrong []string
|
|
counts := map[string]int{}
|
|
for _, p := range paths {
|
|
b, err := os.ReadFile(p)
|
|
if err != nil {
|
|
res.err = err
|
|
return res
|
|
}
|
|
got := strings.TrimSpace(string(b))
|
|
counts[got]++
|
|
if got != want {
|
|
wrong = append(wrong, p)
|
|
}
|
|
}
|
|
if len(wrong) == 0 {
|
|
res.state = fmt.Sprintf("%s on all %d cpus", want, len(paths))
|
|
return res
|
|
}
|
|
var found []string
|
|
for k, v := range counts {
|
|
found = append(found, fmt.Sprintf("%s:%d", k, v))
|
|
}
|
|
for _, p := range wrong {
|
|
if err := os.WriteFile(p, []byte(want), 0o644); err != nil {
|
|
res.err = err
|
|
res.state = fmt.Sprintf("could not set %s", p)
|
|
return res
|
|
}
|
|
}
|
|
res.fixed = true
|
|
res.state = fmt.Sprintf("was %s, now %s on all %d cpus", strings.Join(found, " "), want, len(paths))
|
|
return res
|
|
}
|
|
|
|
func checkLinkUp(fd int, ifname string) checkResult {
|
|
res := checkResult{item: ifname + " link up"}
|
|
var ifr flagsIfreq
|
|
copy(ifr.name[:], ifname)
|
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
|
uintptr(unix.SIOCGIFFLAGS), uintptr(unsafe.Pointer(&ifr)))
|
|
if errno != 0 {
|
|
res.err = errno
|
|
return res
|
|
}
|
|
if ifr.flags&unix.IFF_UP != 0 {
|
|
res.state = "up"
|
|
return res
|
|
}
|
|
ifr.flags |= unix.IFF_UP
|
|
_, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
|
|
uintptr(unix.SIOCSIFFLAGS), uintptr(unsafe.Pointer(&ifr)))
|
|
if errno != 0 {
|
|
res.err = errno
|
|
res.state = "could not set IFF_UP"
|
|
return res
|
|
}
|
|
res.fixed = true
|
|
res.state = "was down, now up"
|
|
return res
|
|
}
|
|
|
|
func checkCoalesce(fd int, ifname string, rxUsecs, txUsecs uint32) checkResult {
|
|
res := checkResult{item: ifname + " coalesce"}
|
|
ec, err := getCoalesce(fd, ifname)
|
|
if err != nil {
|
|
res.err = err
|
|
return res
|
|
}
|
|
desc := func(e ethtoolCoalesce) string {
|
|
return fmt.Sprintf("adaptive rx=%d tx=%d rx-usecs=%d tx-usecs=%d",
|
|
e.useAdaptiveRxCoalesce, e.useAdaptiveTxCoalesce, e.rxCoalesceUsecs, e.txCoalesceUsecs)
|
|
}
|
|
if ec.useAdaptiveRxCoalesce == 0 && ec.useAdaptiveTxCoalesce == 0 &&
|
|
ec.rxCoalesceUsecs == rxUsecs && ec.txCoalesceUsecs == txUsecs {
|
|
res.state = desc(ec)
|
|
return res
|
|
}
|
|
was := desc(ec)
|
|
ec.cmd = unix.ETHTOOL_SCOALESCE
|
|
ec.useAdaptiveRxCoalesce = 0
|
|
ec.useAdaptiveTxCoalesce = 0
|
|
ec.rxCoalesceUsecs = rxUsecs
|
|
ec.txCoalesceUsecs = txUsecs
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&ec)); err != nil {
|
|
res.err = err
|
|
res.state = "could not set"
|
|
return res
|
|
}
|
|
res.fixed = true
|
|
res.state = fmt.Sprintf("was %s, now adaptive off rx-usecs=%d tx-usecs=%d", was, rxUsecs, txUsecs)
|
|
return res
|
|
}
|
|
|
|
func checkRings(fd int, ifname string, rxWant, txWant uint32) checkResult {
|
|
res := checkResult{item: ifname + " rings"}
|
|
rp, err := getRings(fd, ifname)
|
|
if err != nil {
|
|
res.err = err
|
|
return res
|
|
}
|
|
rx := min(rxWant, rp.rxMaxPending)
|
|
tx := min(txWant, rp.txMaxPending)
|
|
if rp.rxPending == rx && rp.txPending == tx {
|
|
res.state = fmt.Sprintf("rx=%d tx=%d", rp.rxPending, rp.txPending)
|
|
return res
|
|
}
|
|
was := fmt.Sprintf("rx=%d tx=%d", rp.rxPending, rp.txPending)
|
|
rp.cmd = unix.ETHTOOL_SRINGPARAM
|
|
rp.rxPending = rx
|
|
rp.txPending = tx
|
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&rp)); err != nil {
|
|
res.err = err
|
|
res.state = "could not set"
|
|
return res
|
|
}
|
|
res.fixed = true
|
|
res.state = fmt.Sprintf("was %s, now rx=%d tx=%d (link reset)", was, rx, tx)
|
|
return res
|
|
}
|
|
|
|
func withIoctlSocket(fn func(fd int) []checkResult) []checkResult {
|
|
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
|
if err != nil {
|
|
return []checkResult{{item: "ioctl socket", err: err}}
|
|
}
|
|
defer unix.Close(fd)
|
|
return fn(fd)
|
|
}
|
|
|
|
// Nothing here waits for a carrier: the two ports are the two ends of the cable
|
|
// under test, so with no cable there is never going to be one.
|
|
func configureSystem(ifnames []string, ethertypes []uint16) []checkResult {
|
|
return withIoctlSocket(func(fd int) []checkResult {
|
|
out := []checkResult{checkGovernor(wantGovernor)}
|
|
for _, ifname := range ifnames {
|
|
out = append(out, checkLinkUp(fd, ifname))
|
|
out = append(out, clearFlowRules(fd, ifname))
|
|
|
|
// Ring changes reprogram the queues, so flow rules pointing at those
|
|
// queues have to be installed afterwards.
|
|
out = append(out, checkRings(fd, ifname, wantRxRing, wantTxRing))
|
|
out = append(out, checkCoalesce(fd, ifname, wantCoalesceUsecs, wantCoalesceUsecs))
|
|
out = append(out, checkTimestamping(fd, ifname))
|
|
out = append(out, checkFlowRules(fd, ifname, ethertypes))
|
|
}
|
|
return out
|
|
})
|
|
}
|