talos/internal/integration/api/ethernet.go
Andrey Smirnov 0419f5d8ba
feat: implement features in ethtool-like support
Support showing current feature state, and changing features on the fly.

The output and interface should be similar to `ethtool`.

We don't support legacy feature names.

```
node: 172.20.0.5
metadata:
    namespace: network
    type: EthernetStatuses.net.talos.dev
    id: enp0s2
    version: 2
    owner: network.EthernetStatusController
    phase: running
    created: 2025-02-10T11:40:32Z
    updated: 2025-02-10T11:40:32Z
spec:
    linkState: true
    port: Other
    duplex: Unknown
    rings:
        rx-max: 256
        tx-max: 256
        rx: 256
        tx: 256
        tx-push: false
        rx-push: false
    features:
        tx-scatter-gather: on
        tx-checksum-ipv4: off [fixed]
        tx-checksum-ip-generic: on
        tx-checksum-ipv6: off [fixed]
        highdma: on [fixed]
        tx-scatter-gather-fraglist: off [fixed]
        tx-vlan-hw-insert: off [fixed]
        rx-vlan-hw-parse: off [fixed]
        rx-vlan-filter: on [fixed]
        vlan-challenged: off [fixed]
        tx-generic-segmentation: on
        rx-gro: on
        rx-lro: off [fixed]
        tx-tcp-segmentation: on
        tx-gso-robust: on [fixed]
        tx-tcp-ecn-segmentation: on
        tx-tcp-mangleid-segmentation: off
        tx-tcp6-segmentation: on
        tx-fcoe-segmentation: off [fixed]
        tx-gre-segmentation: off [fixed]
        tx-gre-csum-segmentation: off [fixed]
        tx-ipxip4-segmentation: off [fixed]
        tx-ipxip6-segmentation: off [fixed]
        tx-udp_tnl-segmentation: off [fixed]
        tx-udp_tnl-csum-segmentation: off [fixed]
        tx-gso-partial: off [fixed]
        tx-tunnel-remcsum-segmentation: off [fixed]
        tx-sctp-segmentation: off [fixed]
        tx-esp-segmentation: off [fixed]
        tx-udp-segmentation: off
        tx-gso-list: off [fixed]
        tx-checksum-fcoe-crc: off [fixed]
        tx-checksum-sctp: off [fixed]
        rx-ntuple-filter: off [fixed]
        rx-hashing: off [fixed]
        rx-checksum: on [fixed]
        tx-nocache-copy: off
        loopback: off [fixed]
        rx-fcs: off [fixed]
        rx-all: off [fixed]
        tx-vlan-stag-hw-insert: off [fixed]
        rx-vlan-stag-hw-parse: off [fixed]
        rx-vlan-stag-filter: off [fixed]
        l2-fwd-offload: off [fixed]
        hw-tc-offload: off [fixed]
        esp-hw-offload: off [fixed]
        esp-tx-csum-hw-offload: off [fixed]
        rx-udp_tunnel-port-offload: off [fixed]
        tls-hw-tx-offload: off [fixed]
        tls-hw-rx-offload: off [fixed]
        rx-gro-hw: on
        tls-hw-record: off [fixed]
        rx-gro-list: off
        macsec-hw-offload: off [fixed]
        rx-udp-gro-forwarding: off
        hsr-tag-ins-offload: off [fixed]
        hsr-tag-rm-offload: off [fixed]
        hsr-fwd-offload: off [fixed]
        hsr-dup-offload: off [fixed]
```

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
2025-02-10 16:05:49 +04:00

200 lines
6.1 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build integration_api
package api
import (
"context"
"os"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/siderolabs/go-pointer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/siderolabs/talos/internal/integration/base"
"github.com/siderolabs/talos/pkg/machinery/client"
networkconfig "github.com/siderolabs/talos/pkg/machinery/config/types/network"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
)
// EthernetSuite ...
type EthernetSuite struct {
base.APISuite
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
// SuiteName ...
func (suite *EthernetSuite) SuiteName() string {
return "api.EthernetSuite"
}
// SetupTest ...
func (suite *EthernetSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 1*time.Minute)
if suite.Cluster == nil || suite.Cluster.Provisioner() != base.ProvisionerQEMU {
suite.T().Skip("skipping ethernet test since provisioner is not qemu")
}
}
// TearDownTest ...
func (suite *EthernetSuite) TearDownTest() {
if suite.ctxCancel != nil {
suite.ctxCancel()
}
}
func getFeatureStatus(t *testing.T, features network.EthernetFeatureStatusList, name string) bool {
t.Helper()
for _, f := range features {
if f.Name == name {
switch f.Status {
case "on":
return true
case "off":
return false
default:
require.Fail(t, "unexpected feature status: %s", f.Status)
}
}
}
require.Fail(t, "feature %s not found", name)
panic("unreachable")
}
// TestEthernetConfig verifies changing Ethernet settings.
func (suite *EthernetSuite) TestEthernetConfig() {
// pick up a random node to test the Ethernet on, and use it throughout the test
node := suite.RandomDiscoveredNodeInternalIP()
suite.T().Logf("testing Ethernet on node %s", node)
// build a Talos API context which is tied to the node
nodeCtx := client.WithNode(suite.ctx, node)
// pick a Ethernet links
ethStatuses, err := safe.StateListAll[*network.EthernetStatus](nodeCtx, suite.Client.COSI)
suite.Require().NoError(err)
var (
linkName string
prevRingConfig *network.EthernetRingsStatus
prevFeatures network.EthernetFeatureStatusList
)
for ethStatus := range ethStatuses.All() {
if ethStatus.TypedSpec().Rings != nil && ethStatus.TypedSpec().Rings.RXMax != nil {
linkName = ethStatus.Metadata().ID()
prevRingConfig = ethStatus.TypedSpec().Rings
prevFeatures = ethStatus.TypedSpec().Features
marshaled, err := resource.MarshalYAML(ethStatus)
suite.Require().NoError(err)
out, err := yaml.Marshal(marshaled)
suite.Require().NoError(err)
suite.T().Logf("found link %s with: %s", linkName, string(out))
break
}
}
suite.Require().NotEmpty(linkName, "no link provides RX rings")
suite.Require().NotEmpty(prevFeatures, "no link provides features")
suite.Run("Rings", func() {
if os.Getenv("CI") != "" {
suite.T().Skip("skipping ethtool test in CI, as QEMU version doesn't support updating RX rings for virtio")
}
// first, adjust RX rings to be 50% of what it was before
newRX := pointer.SafeDeref(prevRingConfig.RXMax) / 2
suite.T().Logf("testing RX rings on link %s: %d -> %d", linkName, pointer.SafeDeref(prevRingConfig.RX), newRX)
cfgDocument := networkconfig.NewEthernetConfigV1Alpha1(linkName)
cfgDocument.RingsConfig = &networkconfig.EthernetRingsConfig{
RX: pointer.To(newRX),
}
suite.PatchMachineConfig(nodeCtx, cfgDocument)
// now EthernetStatus should reflect the new RX rings
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, linkName,
func(ethStatus *network.EthernetStatus, asrt *assert.Assertions) {
asrt.Equal(newRX, pointer.SafeDeref(ethStatus.TypedSpec().Rings.RX))
},
)
// now, let's revert the RX rings to what it was before
cfgDocument.RingsConfig.RX = prevRingConfig.RX
suite.PatchMachineConfig(nodeCtx, cfgDocument)
// now EthernetStatus should reflect the new RX rings
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, linkName,
func(ethStatus *network.EthernetStatus, asrt *assert.Assertions) {
asrt.Equal(pointer.SafeDeref(prevRingConfig.RX), pointer.SafeDeref(ethStatus.TypedSpec().Rings.RX))
},
)
// remove the config document
suite.RemoveMachineConfigDocuments(nodeCtx, cfgDocument.MetaKind)
})
suite.Run("Features", func() {
const featureName = "tx-tcp-segmentation"
// get the initial state
initialState := getFeatureStatus(suite.T(), prevFeatures, featureName)
suite.T().Logf("testing feature %s on link %s: %v -> %v", featureName, linkName, initialState, !initialState)
cfgDocument := networkconfig.NewEthernetConfigV1Alpha1(linkName)
cfgDocument.FeaturesConfig = map[string]bool{
featureName: !initialState,
}
suite.PatchMachineConfig(nodeCtx, cfgDocument)
// now EthernetStatus should reflect the new feature status
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, linkName,
func(ethStatus *network.EthernetStatus, asrt *assert.Assertions) {
asrt.Equal(!initialState, getFeatureStatus(suite.T(), ethStatus.TypedSpec().Features, featureName))
},
)
// now, let's revert the RX rings to what it was before
cfgDocument.FeaturesConfig[featureName] = initialState
suite.PatchMachineConfig(nodeCtx, cfgDocument)
// now EthernetStatus should reflect the old feature status
rtestutils.AssertResource(nodeCtx, suite.T(), suite.Client.COSI, linkName,
func(ethStatus *network.EthernetStatus, asrt *assert.Assertions) {
asrt.Equal(initialState, getFeatureStatus(suite.T(), ethStatus.TypedSpec().Features, featureName))
},
)
// remove the config document
suite.RemoveMachineConfigDocuments(nodeCtx, cfgDocument.MetaKind)
})
}
func init() {
allSuites = append(allSuites, new(EthernetSuite))
}