fix: correctly map link names/aliases when using VIP operator

Also fix/clean up tests, and add more tests specific to link aliases.

Fixes #10402

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2025-02-26 14:27:14 +04:00
parent 7c4e47c0c0
commit d45eaeb74c
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
17 changed files with 728 additions and 480 deletions

View File

@ -205,7 +205,7 @@ func (ctrl *AddressConfigController) parseCmdline(logger *zap.Logger) (addresses
return
}
settings, err := ParseCmdlineNetwork(ctrl.Cmdline)
settings, err := ParseCmdlineNetwork(ctrl.Cmdline, network.NewEmptyLinkResolver())
if err != nil {
logger.Info("ignoring cmdline parse failure", zap.Error(err))

View File

@ -15,6 +15,8 @@ import (
"strings"
"github.com/siderolabs/gen/pair/ordered"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-procfs/procfs"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
@ -41,30 +43,6 @@ type CmdlineLinkConfig struct {
DHCP bool
}
func (linkConfig *CmdlineLinkConfig) resolveLinkName() error {
if !strings.HasPrefix(linkConfig.LinkName, "enx") {
return nil
}
ifaces, _ := net.Interfaces() //nolint:errcheck // ignoring error here as ifaces will be empty
mac := strings.ToLower(strings.TrimPrefix(linkConfig.LinkName, "enx"))
for _, iface := range ifaces {
ifaceMAC := strings.ReplaceAll(iface.HardwareAddr.String(), ":", "")
if ifaceMAC == mac {
linkConfig.LinkName = iface.Name
return nil
}
}
if strings.HasPrefix(linkConfig.LinkName, "enx") {
return fmt.Errorf("cmdline device parse failure: interface by MAC not found %s", linkConfig.LinkName)
}
return nil
}
// splitIPArgument splits the `ip=` kernel argument honoring the IPv6 addresses in square brackets.
func splitIPArgument(val string) []string {
var (
@ -98,7 +76,7 @@ const autoconfDHCP = "dhcp"
// ParseCmdlineNetwork parses `ip=` and Talos specific kernel cmdline argument producing all the available configuration options.
//
//nolint:gocyclo,cyclop
func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
func ParseCmdlineNetwork(cmdline *procfs.Cmdline, linkNameResolver *network.LinkResolver) (CmdlineNetworking, error) {
var (
settings CmdlineNetworking
err error
@ -113,7 +91,7 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
ignoreInterfaces := cmdline.Get(constants.KernelParamNetworkInterfaceIgnore)
for i := 0; ignoreInterfaces.Get(i) != nil; i++ {
settings.IgnoreInterfaces = append(settings.IgnoreInterfaces, *ignoreInterfaces.Get(i))
settings.IgnoreInterfaces = append(settings.IgnoreInterfaces, linkNameResolver.Resolve(*ignoreInterfaces.Get(i)))
}
// standard ip=
@ -135,14 +113,10 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
case len(fields) == 2 && fields[1] == autoconfDHCP:
// ip=<device>:dhcp
linkConfig := CmdlineLinkConfig{
LinkName: fields[0],
LinkName: linkNameResolver.Resolve(fields[0]),
DHCP: true,
}
if err = linkConfig.resolveLinkName(); err != nil {
return settings, err
}
linkSpecSpecs = append(linkSpecSpecs, network.LinkSpecSpec{
Name: linkConfig.LinkName,
Up: true,
@ -196,7 +170,7 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
settings.Hostname = fields[4]
}
case 5:
linkConfig.LinkName = fields[5]
linkConfig.LinkName = linkNameResolver.Resolve(fields[5])
case 6:
if fields[6] == autoconfDHCP {
linkConfig.DHCP = true
@ -222,11 +196,6 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
}
}
// resolve enx* (with MAC address) to the actual interface name
if err = linkConfig.resolveLinkName(); err != nil {
return settings, err
}
// if interface name is not set, pick the first non-loopback interface
if linkConfig.LinkName == "" {
ifaces, _ := net.Interfaces() //nolint:errcheck // ignoring error here as ifaces will be empty
@ -297,6 +266,9 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
}
}
// resolve bond slave names via aliases as needed
bondSlaves = xslices.Map(bondSlaves, linkNameResolver.Resolve)
bondLinkSpec := network.LinkSpecSpec{
Name: bondName,
Up: true,
@ -328,6 +300,7 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
linkSpecSpecs = append(linkSpecSpecs, slaveLinkSpec)
}
}
// dracut vlan=<vlanname>:<phydevice>
vlanSettings := cmdline.Get(constants.KernelParamVlan).First()
if vlanSettings != nil {
@ -361,11 +334,13 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
vlanName = nethelpers.VLANLinkName(phyDevice, uint16(vlanID))
phyDevice = linkNameResolver.Resolve(phyDevice)
linkSpecUpdated := false
for i, linkSpec := range linkSpecSpecs {
if linkSpec.Name == vlanName {
vlanLink(&linkSpecSpecs[i], phyDevice, vlanSpec)
vlanLink(&linkSpecSpecs[i], vlanName, phyDevice, vlanSpec)
linkSpecUpdated = true
@ -380,7 +355,7 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
ConfigLayer: network.ConfigCmdline,
}
vlanLink(&linkSpec, phyDevice, vlanSpec)
vlanLink(&linkSpec, vlanName, phyDevice, vlanSpec)
linkSpecSpecs = append(linkSpecSpecs, linkSpec)
}
@ -509,8 +484,7 @@ func parseBondOptions(options string) (v1alpha1.Bond, error) {
}
if useCarrier == 1 {
val := []bool{true}
bond.BondUseCarrier = &val[0]
bond.BondUseCarrier = pointer.To(true)
}
default:
return bond, fmt.Errorf("unknown bond option: %s", optionPair[0])

View File

@ -7,24 +7,24 @@ package network_test
import (
"cmp"
"fmt"
"iter"
"net"
"net/netip"
"slices"
"testing"
"github.com/siderolabs/go-procfs/procfs"
"github.com/stretchr/testify/suite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network"
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
netconfig "github.com/siderolabs/talos/pkg/machinery/resources/network"
)
type CmdlineSuite struct {
suite.Suite
}
func TestCmdlineParse(t *testing.T) {
t.Parallel()
func (suite *CmdlineSuite) TestParse() {
ifaces, _ := net.Interfaces() //nolint:errcheck // ignoring error here as ifaces will be empty
slices.SortFunc(ifaces, func(a, b net.Interface) int { return cmp.Compare(a.Name, b.Name) })
@ -44,7 +44,7 @@ func (suite *CmdlineSuite) TestParse() {
defaultBondSettings := network.CmdlineNetworking{
NetworkLinkSpecs: []netconfig.LinkSpecSpec{
{
Name: "bond0",
Name: "bond1",
Kind: "bond",
Type: nethelpers.LinkEther,
Logical: true,
@ -66,7 +66,7 @@ func (suite *CmdlineSuite) TestParse() {
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
BondSlave: netconfig.BondSlave{
MasterName: "bond0",
MasterName: "bond1",
SlaveIndex: 0,
},
},
@ -76,7 +76,7 @@ func (suite *CmdlineSuite) TestParse() {
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
BondSlave: netconfig.BondSlave{
MasterName: "bond0",
MasterName: "bond1",
SlaveIndex: 1,
},
},
@ -140,7 +140,22 @@ func (suite *CmdlineSuite) TestParse() {
name: "no iface by mac address",
cmdline: "ip=172.20.0.2::172.20.0.1:255.255.255.0::enx001122aabbcc",
expectedError: "cmdline device parse failure: interface by MAC not found enx001122aabbcc",
expectedSettings: network.CmdlineNetworking{
LinkConfigs: []network.CmdlineLinkConfig{
{
Address: netip.MustParsePrefix("172.20.0.2/24"),
Gateway: netip.MustParseAddr("172.20.0.1"),
LinkName: "enx001122aabbcc",
},
},
NetworkLinkSpecs: []netconfig.LinkSpecSpec{
{
Name: "enx001122aabbcc",
Up: true,
ConfigLayer: netconfig.ConfigCmdline,
},
},
},
},
{
name: "complete",
@ -293,20 +308,20 @@ func (suite *CmdlineSuite) TestParse() {
},
{
name: "ignore interfaces",
cmdline: "talos.network.interface.ignore=eth2 talos.network.interface.ignore=eth3",
cmdline: "talos.network.interface.ignore=eth2 talos.network.interface.ignore=eth3 talos.network.interface.ignore=enxa",
expectedSettings: network.CmdlineNetworking{
IgnoreInterfaces: []string{"eth2", "eth3"},
IgnoreInterfaces: []string{"eth2", "eth3", "eth31"},
},
},
{
name: "bond with no interfaces and no options set",
cmdline: "bond=bond0",
cmdline: "bond=bond1",
expectedSettings: defaultBondSettings,
},
{
name: "bond with no interfaces and empty options set",
cmdline: "bond=bond0:::",
cmdline: "bond=bond1:::",
expectedSettings: defaultBondSettings,
},
{
@ -314,22 +329,7 @@ func (suite *CmdlineSuite) TestParse() {
cmdline: "bond=bond1:eth3,eth4",
expectedSettings: network.CmdlineNetworking{
NetworkLinkSpecs: []netconfig.LinkSpecSpec{
{
Name: "bond1",
Kind: "bond",
Type: nethelpers.LinkEther,
Logical: true,
Up: true,
ConfigLayer: netconfig.ConfigCmdline,
BondMaster: netconfig.BondMasterSpec{
ResendIGMP: 1,
LPInterval: 1,
PacketsPerSlave: 1,
NumPeerNotif: 1,
TLBDynamicLB: 1,
UseCarrier: true,
},
},
defaultBondSettings.NetworkLinkSpecs[0],
{
Name: "eth3",
Up: true,
@ -353,6 +353,35 @@ func (suite *CmdlineSuite) TestParse() {
},
},
},
{
name: "bond with aliased interfaces",
cmdline: "bond=bond1:enxa,enxb",
expectedSettings: network.CmdlineNetworking{
NetworkLinkSpecs: []netconfig.LinkSpecSpec{
defaultBondSettings.NetworkLinkSpecs[0],
{
Name: "eth31",
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
BondSlave: netconfig.BondSlave{
MasterName: "bond1",
SlaveIndex: 0,
},
},
{
Name: "eth32",
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
BondSlave: netconfig.BondSlave{
MasterName: "bond1",
SlaveIndex: 1,
},
},
},
},
},
{
name: "bond with interfaces, options and mtu set",
cmdline: "bond=bond1:eth3,eth4:mode=802.3ad,xmit_hash_policy=layer2+3:1450",
@ -498,6 +527,27 @@ func (suite *CmdlineSuite) TestParse() {
},
},
},
{
name: "vlan configuration with alias link name",
cmdline: "vlan=vlan4095:enxa",
expectedSettings: network.CmdlineNetworking{
NetworkLinkSpecs: []netconfig.LinkSpecSpec{
{
Name: "enxa.4095",
Logical: true,
Up: true,
Kind: netconfig.LinkKindVLAN,
Type: nethelpers.LinkEther,
ParentName: "eth31",
ConfigLayer: netconfig.ConfigCmdline,
VLAN: netconfig.VLANSpec{
VID: 4095,
Protocol: nethelpers.VLANProtocol8021Q,
},
},
},
},
},
{
name: "vlan configuration with invalid vlan ID 4096",
cmdline: "vlan=eth1.4096:eth1",
@ -548,21 +598,31 @@ func (suite *CmdlineSuite) TestParse() {
},
},
} {
suite.Run(test.name, func() {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
cmdline := procfs.NewCmdline(test.cmdline)
settings, err := network.ParseCmdlineNetwork(cmdline)
link1 := netconfig.NewLinkStatus(netconfig.NamespaceName, "eth31")
link1.TypedSpec().Alias = "enxa"
link2 := netconfig.NewLinkStatus(netconfig.NamespaceName, "eth32")
link2.TypedSpec().Alias = "enxb"
settings, err := network.ParseCmdlineNetwork(
cmdline,
netconfig.NewLinkResolver(
func() iter.Seq[*netconfig.LinkStatus] {
return slices.Values([]*netconfig.LinkStatus{link1, link2})
},
),
)
if test.expectedError != "" {
suite.Assert().EqualError(err, test.expectedError)
assert.EqualError(t, err, test.expectedError)
} else {
suite.Assert().NoError(err)
suite.Assert().Equal(test.expectedSettings, settings)
require.NoError(t, err)
assert.Equal(t, test.expectedSettings, settings)
}
})
}
}
func TestCmdlineSuite(t *testing.T) {
suite.Run(t, new(CmdlineSuite))
}

View File

@ -10,7 +10,6 @@ import (
"net/netip"
"slices"
"strings"
"sync"
"testing"
"time"
@ -19,9 +18,9 @@ import (
"github.com/cosi-project/runtime/pkg/safe"
"github.com/miekg/dns"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/gen/xtesting/must"
"github.com/siderolabs/go-retry/retry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/zap/zaptest"
@ -46,7 +45,7 @@ func expectedDNSRunners(port string) []resource.ID {
func (suite *DNSServer) TestResolving() {
dnsSlice := []string{"8.8.8.8", "1.1.1.1"}
port := must.Value(getDynamicPort())(suite.T())
port := getDynamicPort(suite.T())
cfg := network.NewHostDNSConfig(network.HostDNSConfigID)
cfg.TypedSpec().Enabled = true
@ -104,7 +103,7 @@ func (suite *DNSServer) TestResolving() {
func (suite *DNSServer) TestSetupStartStop() {
dnsSlice := []string{"8.8.8.8", "1.1.1.1"}
port := must.Value(getDynamicPort())(suite.T())
port := getDynamicPort(suite.T())
resolverSpec := network.NewResolverStatus(network.NamespaceName, network.ResolverID)
resolverSpec.TypedSpec().DNSServers = xslices.Map(dnsSlice, netip.MustParseAddr)
@ -148,7 +147,7 @@ func (suite *DNSServer) TestSetupStartStop() {
}
func (suite *DNSServer) TestResolveMembers() {
port := must.Value(getDynamicPort())(suite.T())
port := getDynamicPort(suite.T())
const (
id = "talos-default-controlplane-1"
@ -264,22 +263,20 @@ func TestDNSServer(t *testing.T) {
})
}
func getDynamicPort() (string, error) {
func getDynamicPort(t *testing.T) string {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
require.NoError(t, err)
closeOnce := sync.OnceValue(l.Close)
addr := l.Addr().String()
defer closeOnce() //nolint:errcheck
require.NoError(t, l.Close())
_, port, err := net.SplitHostPort(l.Addr().String())
if err != nil {
return "", err
}
_, port, err := net.SplitHostPort(addr)
require.NoError(t, err)
return port, closeOnce()
return port
}
func makeAddrs(port string) []netip.AddrPort {
@ -293,7 +290,7 @@ type DNSUpstreams struct {
}
func (suite *DNSUpstreams) TestOrder() {
port := must.Value(getDynamicPort())(suite.T())
port := getDynamicPort(suite.T())
cfg := network.NewHostDNSConfig(network.HostDNSConfigID)
cfg.TypedSpec().Enabled = true

View File

@ -230,7 +230,7 @@ func (ctrl *HostnameConfigController) parseCmdline(logger *zap.Logger) (spec net
return
}
settings, err := ParseCmdlineNetwork(ctrl.Cmdline)
settings, err := ParseCmdlineNetwork(ctrl.Cmdline, network.NewEmptyLinkResolver())
if err != nil {
logger.Warn("ignoring error", zap.Error(err))

View File

@ -92,6 +92,13 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime,
}
}
linkStatuses, err := safe.ReaderListAll[*network.LinkStatus](ctx, r)
if err != nil {
return fmt.Errorf("error listing link statuses: %w", err)
}
linkNameResolver := network.NewLinkResolver(linkStatuses.All)
// bring up loopback interface
{
var ids []string
@ -113,7 +120,7 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime,
}
// parse kernel cmdline for the interface name
cmdlineLinks, cmdlineIgnored := ctrl.parseCmdline(logger)
cmdlineLinks, cmdlineIgnored := ctrl.parseCmdline(logger, linkNameResolver)
for _, cmdlineLink := range cmdlineLinks {
if cmdlineLink.Name != "" {
if _, ignored := ignoredInterfaces[cmdlineLink.Name]; !ignored {
@ -133,7 +140,7 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime,
// parse machine configuration for link specs
if len(devices) > 0 {
links := ctrl.processDevicesConfiguration(logger, devices)
links := ctrl.processDevicesConfiguration(logger, devices, linkNameResolver)
var ids []string
@ -178,27 +185,10 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime,
}
}
list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.LinkStatusType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing link statuses: %w", err)
}
outer:
for _, item := range list.Items {
linkStatus := item.(*network.LinkStatus) //nolint:forcetypeassert
if _, configured := configuredLinks[linkStatus.Metadata().ID()]; configured {
continue
}
if linkStatus.TypedSpec().Alias != "" {
if _, configured := configuredLinks[linkStatus.TypedSpec().Alias]; configured {
continue
}
}
for _, altName := range linkStatus.TypedSpec().AltNames {
if _, configured := configuredLinks[altName]; configured {
for linkStatus := range linkStatuses.All() {
for linkAlias := range network.AllLinkNames(linkStatus) {
if _, configured := configuredLinks[linkAlias]; configured {
continue outer
}
}
@ -223,13 +213,13 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime,
}
}
// list links for cleanup
list, err = r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined))
// list link specs for cleanup
linkSpecs, err := safe.ReaderList[*network.LinkSpec](ctx, r, resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for _, res := range list.Items {
for res := range linkSpecs.All() {
if res.Metadata().Owner() != ctrl.Name() {
// skip specs created by other controllers
continue
@ -271,12 +261,12 @@ func (ctrl *LinkConfigController) apply(ctx context.Context, r controller.Runtim
return ids, nil
}
func (ctrl *LinkConfigController) parseCmdline(logger *zap.Logger) ([]network.LinkSpecSpec, []string) {
func (ctrl *LinkConfigController) parseCmdline(logger *zap.Logger, linkNameResolver *network.LinkResolver) ([]network.LinkSpecSpec, []string) {
if ctrl.Cmdline == nil {
return []network.LinkSpecSpec{}, nil
}
settings, err := ParseCmdlineNetwork(ctrl.Cmdline)
settings, err := ParseCmdlineNetwork(ctrl.Cmdline, linkNameResolver)
if err != nil {
logger.Info("ignoring error", zap.Error(err))
@ -287,7 +277,7 @@ func (ctrl *LinkConfigController) parseCmdline(logger *zap.Logger) ([]network.Li
}
//nolint:gocyclo,cyclop
func (ctrl *LinkConfigController) processDevicesConfiguration(logger *zap.Logger, devices []talosconfig.Device) []network.LinkSpecSpec {
func (ctrl *LinkConfigController) processDevicesConfiguration(logger *zap.Logger, devices []talosconfig.Device, linkNameResolver *network.LinkResolver) []network.LinkSpecSpec {
// scan for the bonds or bridges
bondedLinks := map[string]ordered.Pair[string, int]{} // mapping physical interface -> bond interface
bridgedLinks := map[string]string{} // mapping physical interface -> bridge interface
@ -297,50 +287,58 @@ func (ctrl *LinkConfigController) processDevicesConfiguration(logger *zap.Logger
continue
}
deviceInterface := linkNameResolver.Resolve(device.Interface())
if device.Bond() != nil {
for idx, linkName := range device.Bond().Interfaces() {
if bondData, exists := bondedLinks[linkName]; exists && bondData.F1 != device.Interface() {
linkName = linkNameResolver.Resolve(linkName)
if bondData, exists := bondedLinks[linkName]; exists && bondData.F1 != deviceInterface {
logger.Sugar().Warnf("link %q is included in both bonds %q and %q", linkName,
bondData.F1, device.Interface())
bondData.F1, deviceInterface)
}
if bridgeName, exists := bridgedLinks[linkName]; exists {
logger.Sugar().Warnf("link %q is included in both bond %q and bridge %q", linkName,
bridgeName, device.Interface())
bridgeName, deviceInterface)
}
bondedLinks[linkName] = ordered.MakePair(device.Interface(), idx)
bondedLinks[linkName] = ordered.MakePair(deviceInterface, idx)
}
}
if device.Bridge() != nil {
for _, linkName := range device.Bridge().Interfaces() {
if bridgeName, exists := bridgedLinks[linkName]; exists && bridgeName != device.Interface() {
linkName = linkNameResolver.Resolve(linkName)
if bridgeName, exists := bridgedLinks[linkName]; exists && bridgeName != deviceInterface {
logger.Sugar().Warnf("link %q is included in both bridges %q and %q", linkName,
bridgeName, device.Interface())
bridgeName, deviceInterface)
}
if bondData, exists := bondedLinks[linkName]; exists {
logger.Sugar().Warnf("link %q is included in both bond %q and bridge %q", linkName,
bondData.F1, device.Interface())
bondData.F1, deviceInterface)
}
bridgedLinks[linkName] = device.Interface()
bridgedLinks[linkName] = deviceInterface
}
}
if device.BridgePort() != nil {
if bridgeName, exists := bridgedLinks[device.Interface()]; exists && bridgeName != device.BridgePort().Master() {
logger.Sugar().Warnf("link %q is included in both bridges %q and %q", device.Interface(),
bridgeName, device.BridgePort().Master())
bridgePortMaster := linkNameResolver.Resolve(device.BridgePort().Master())
if bridgeName, exists := bridgedLinks[deviceInterface]; exists && bridgeName != bridgePortMaster {
logger.Sugar().Warnf("link %q is included in both bridges %q and %q", deviceInterface,
bridgeName, bridgePortMaster)
}
if bondData, exists := bondedLinks[device.Interface()]; exists {
logger.Sugar().Warnf("link %q is included into both bond %q and bridge %q", device.Interface(),
bondData.F1, device.BridgePort().Master())
if bondData, exists := bondedLinks[deviceInterface]; exists {
logger.Sugar().Warnf("link %q is included into both bond %q and bridge %q", deviceInterface,
bondData.F1, bridgePortMaster)
}
bridgedLinks[device.Interface()] = device.BridgePort().Master()
bridgedLinks[deviceInterface] = bridgePortMaster
}
}
@ -351,50 +349,51 @@ func (ctrl *LinkConfigController) processDevicesConfiguration(logger *zap.Logger
continue
}
if _, exists := linkMap[device.Interface()]; !exists {
linkMap[device.Interface()] = &network.LinkSpecSpec{
Name: device.Interface(),
deviceInterface := linkNameResolver.Resolve(device.Interface())
if _, exists := linkMap[deviceInterface]; !exists {
linkMap[deviceInterface] = &network.LinkSpecSpec{
Name: deviceInterface,
Up: true,
ConfigLayer: network.ConfigMachineConfiguration,
}
}
if device.MTU() != 0 {
linkMap[device.Interface()].MTU = uint32(device.MTU())
linkMap[deviceInterface].MTU = uint32(device.MTU())
}
if device.Bond() != nil {
if err := SetBondMaster(linkMap[device.Interface()], device.Bond()); err != nil {
if err := SetBondMaster(linkMap[deviceInterface], device.Bond()); err != nil {
logger.Error("error parsing bond config", zap.Error(err))
}
}
if device.Bridge() != nil {
if err := SetBridgeMaster(linkMap[device.Interface()], device.Bridge()); err != nil {
if err := SetBridgeMaster(linkMap[deviceInterface], device.Bridge()); err != nil {
logger.Error("error parsing bridge config", zap.Error(err))
}
}
if device.WireguardConfig() != nil {
if err := wireguardLink(linkMap[device.Interface()], device.WireguardConfig()); err != nil {
if err := wireguardLink(linkMap[deviceInterface], device.WireguardConfig()); err != nil {
logger.Error("error parsing wireguard config", zap.Error(err))
}
}
if device.Dummy() {
dummyLink(linkMap[device.Interface()])
dummyLink(linkMap[deviceInterface])
}
for _, vlan := range device.Vlans() {
vlanName := nethelpers.VLANLinkName(device.Interface(), vlan.ID())
vlanName := nethelpers.VLANLinkName(device.Interface(), vlan.ID()) // [NOTE]: VLAN uses the original interface name (before resolving aliases)
linkMap[vlanName] = &network.LinkSpecSpec{
Name: device.Interface(),
Up: true,
ConfigLayer: network.ConfigMachineConfiguration,
}
vlanLink(linkMap[vlanName], device.Interface(), vlan)
vlanLink(linkMap[vlanName], vlanName, deviceInterface, vlan)
}
}
@ -430,8 +429,8 @@ type vlaner interface {
MTU() uint32
}
func vlanLink(link *network.LinkSpecSpec, linkName string, vlan vlaner) {
link.Name = nethelpers.VLANLinkName(linkName, vlan.ID())
func vlanLink(link *network.LinkSpecSpec, vlanName, linkName string, vlan vlaner) {
link.Name = vlanName
link.Logical = true
link.Up = true
link.MTU = vlan.MTU()

View File

@ -2,30 +2,22 @@
// 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/.
//nolint:dupl,goconst
//nolint:goconst
package network_test
import (
"context"
"net/netip"
"net/url"
"sync"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-procfs/procfs"
"github.com/siderolabs/go-retry/retry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"go.uber.org/zap/zaptest"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
netctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network"
"github.com/siderolabs/talos/pkg/machinery/config/container"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
@ -35,73 +27,21 @@ import (
)
type LinkConfigSuite struct {
suite.Suite
state state.State
runtime *runtime.Runtime
wg sync.WaitGroup
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
func (suite *LinkConfigSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
var err error
suite.runtime, err = runtime.NewRuntime(suite.state, zaptest.NewLogger(suite.T()))
suite.Require().NoError(err)
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.DeviceConfigController{}))
}
func (suite *LinkConfigSuite) startRuntime() {
suite.wg.Add(1)
go func() {
defer suite.wg.Done()
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
ctest.DefaultSuite
}
func (suite *LinkConfigSuite) assertLinks(requiredIDs []string, check func(*network.LinkSpec, *assert.Assertions)) {
assertResources(suite.ctx, suite.T(), suite.state, requiredIDs, check, rtestutils.WithNamespace(network.ConfigNamespaceName))
ctest.AssertResources(suite, requiredIDs, check, rtestutils.WithNamespace(network.ConfigNamespaceName))
}
func (suite *LinkConfigSuite) assertNoLinks(unexpectedIDs []string) error {
unexpIDs := make(map[string]struct{}, len(unexpectedIDs))
func (suite *LinkConfigSuite) assertNoLinks(unexpectedIDs []string) {
for _, id := range unexpectedIDs {
unexpIDs[id] = struct{}{}
ctest.AssertNoResource[*network.LinkSpec](suite, id, rtestutils.WithNamespace(network.ConfigNamespaceName))
}
resources, err := suite.state.List(
suite.ctx,
resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined),
)
if err != nil {
return err
}
for _, res := range resources.Items {
_, unexpected := unexpIDs[res.Metadata().ID()]
if unexpected {
return retry.ExpectedErrorf("unexpected ID %q", res.Metadata().ID())
}
}
return nil
}
func (suite *LinkConfigSuite) TestLoopback() {
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.LinkConfigController{}))
suite.startRuntime()
suite.Require().NoError(suite.Runtime().RegisterController(&netctrl.LinkConfigController{}))
suite.assertLinks(
[]string{
@ -117,15 +57,13 @@ func (suite *LinkConfigSuite) TestLoopback() {
func (suite *LinkConfigSuite) TestCmdline() {
suite.Require().NoError(
suite.runtime.RegisterController(
suite.Runtime().RegisterController(
&netctrl.LinkConfigController{
Cmdline: procfs.NewCmdline("ip=172.20.0.2::172.20.0.1:255.255.255.0::eth1:::::"),
},
),
)
suite.startRuntime()
suite.assertLinks(
[]string{
"cmdline/eth1",
@ -142,9 +80,7 @@ func (suite *LinkConfigSuite) TestCmdline() {
func (suite *LinkConfigSuite) TestMachineConfiguration() {
const kernelDriver = "somekerneldriver"
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.LinkConfigController{}))
suite.startRuntime()
suite.Require().NoError(suite.Runtime().RegisterController(&netctrl.LinkConfigController{}))
u, err := url.Parse("https://foo:6443")
suite.Require().NoError(err)
@ -278,13 +214,13 @@ func (suite *LinkConfigSuite) TestMachineConfiguration() {
),
)
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
suite.Create(cfg)
for _, name := range []string{"eth6", "eth7"} {
status := network.NewLinkStatus(network.NamespaceName, name)
status.TypedSpec().Driver = kernelDriver
suite.Require().NoError(suite.state.Create(suite.ctx, status))
suite.Create(status)
}
suite.assertLinks(
@ -401,9 +337,130 @@ func (suite *LinkConfigSuite) TestMachineConfiguration() {
)
}
func (suite *LinkConfigSuite) TestMachineConfigurationWithAliases() {
suite.Require().NoError(suite.Runtime().RegisterController(&netctrl.LinkConfigController{}))
u, err := url.Parse("https://foo:6443")
suite.Require().NoError(err)
cfg := config.NewMachineConfig(
container.NewV1Alpha1(
&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
NetworkInterfaces: []*v1alpha1.Device{
{
DeviceInterface: "enx0123",
DeviceVlans: []*v1alpha1.Vlan{
{
VlanID: 24,
VlanMTU: 1000,
},
},
},
{
DeviceInterface: "enx0123",
DeviceMTU: 9001,
},
{
DeviceIgnore: pointer.To(true),
DeviceInterface: "enx0456",
},
{
DeviceInterface: "bond0",
DeviceBond: &v1alpha1.Bond{
BondInterfaces: []string{"enxa", "enxb"},
BondMode: "balance-xor",
},
},
},
},
},
ClusterConfig: &v1alpha1.ClusterConfig{
ControlPlane: &v1alpha1.ControlPlaneConfig{
Endpoint: &v1alpha1.Endpoint{
URL: u,
},
},
},
},
),
)
suite.Create(cfg)
for _, link := range []struct {
name string
aliases []string
}{
{
name: "eth0",
aliases: []string{"enx0123"},
},
{
name: "eth1",
aliases: []string{"enx0456"},
},
{
name: "eth2",
aliases: []string{"enxa"},
},
{
name: "eth3",
aliases: []string{"enxb"},
},
} {
status := network.NewLinkStatus(network.NamespaceName, link.name)
status.TypedSpec().AltNames = link.aliases
suite.Create(status)
}
suite.assertLinks(
[]string{
"configuration/eth0",
"configuration/enx0123.24",
"configuration/eth2",
"configuration/eth3",
"configuration/bond0",
}, func(r *network.LinkSpec, asrt *assert.Assertions) {
asrt.Equal(network.ConfigMachineConfiguration, r.TypedSpec().ConfigLayer)
switch r.TypedSpec().Name {
case "eth0":
asrt.True(r.TypedSpec().Up)
asrt.False(r.TypedSpec().Logical)
asrt.EqualValues(9001, r.TypedSpec().MTU)
case "eth2", "eth3":
asrt.True(r.TypedSpec().Up)
asrt.False(r.TypedSpec().Logical)
asrt.Equal("bond0", r.TypedSpec().BondSlave.MasterName)
case "eth0.24":
asrt.True(r.TypedSpec().Up)
asrt.True(r.TypedSpec().Logical)
asrt.Equal(nethelpers.LinkEther, r.TypedSpec().Type)
asrt.Equal(network.LinkKindVLAN, r.TypedSpec().Kind)
asrt.Equal("eth0", r.TypedSpec().ParentName)
asrt.Equal(nethelpers.VLANProtocol8021Q, r.TypedSpec().VLAN.Protocol)
asrt.EqualValues(24, r.TypedSpec().VLAN.VID)
asrt.EqualValues(1000, r.TypedSpec().MTU)
case "bond0":
asrt.True(r.TypedSpec().Up)
asrt.True(r.TypedSpec().Logical)
asrt.Equal(nethelpers.LinkEther, r.TypedSpec().Type)
asrt.Equal(network.LinkKindBond, r.TypedSpec().Kind)
asrt.Equal(nethelpers.BondModeXOR, r.TypedSpec().BondMaster.Mode)
asrt.True(r.TypedSpec().BondMaster.UseCarrier)
}
},
)
}
func (suite *LinkConfigSuite) TestDefaultUp() {
suite.Require().NoError(
suite.runtime.RegisterController(
suite.Runtime().RegisterController(
&netctrl.LinkConfigController{
Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth2"),
},
@ -419,7 +476,7 @@ func (suite *LinkConfigSuite) TestDefaultUp() {
linkStatus.TypedSpec().AltNames = []string{"eth0"}
}
suite.Require().NoError(suite.state.Create(suite.ctx, linkStatus))
suite.Create(linkStatus)
}
u, err := url.Parse("https://foo:6443")
@ -472,9 +529,7 @@ func (suite *LinkConfigSuite) TestDefaultUp() {
),
)
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
suite.startRuntime()
suite.Create(cfg)
suite.assertLinks(
[]string{
@ -485,30 +540,25 @@ func (suite *LinkConfigSuite) TestDefaultUp() {
},
)
suite.Assert().NoError(
retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertNoLinks(
[]string{
"default/eth0",
"default/eth2",
"default/eth3",
"default/eth4",
},
)
},
),
suite.assertNoLinks(
[]string{
"default/eth0",
"default/eth2",
"default/eth3",
"default/eth4",
},
)
}
func (suite *LinkConfigSuite) TearDownTest() {
suite.T().Log("tear down")
suite.ctxCancel()
suite.wg.Wait()
}
func TestLinkConfigSuite(t *testing.T) {
suite.Run(t, new(LinkConfigSuite))
t.Parallel()
suite.Run(t, &LinkConfigSuite{
DefaultSuite: ctest.DefaultSuite{
Timeout: 5 * time.Second,
AfterSetup: func(s *ctest.DefaultSuite) {
s.Require().NoError(s.Runtime().RegisterController(&netctrl.DeviceConfigController{}))
},
},
})
}

View File

@ -81,38 +81,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
return fmt.Errorf("error listing link statuses: %w", err)
}
// build an alias/altname map and a list of all interfaces
linkAliasMap := map[string]string{}
for linkStatus := range linkStatuses.All() {
if linkStatus.TypedSpec().Alias != "" {
linkAliasMap[linkStatus.TypedSpec().Alias] = linkStatus.Metadata().ID()
}
for _, altName := range linkStatus.TypedSpec().AltNames {
linkAliasMap[altName] = linkStatus.Metadata().ID()
}
}
// direct names override aliases
for linkStatus := range linkStatuses.All() {
linkAliasMap[linkStatus.Metadata().ID()] = linkStatus.Metadata().ID()
}
lookupLinkName := func(linkName string) string {
if alias, ok := linkAliasMap[linkName]; ok {
return alias
}
return linkName
}
items, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.DeviceConfigSpecType, "", resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting config: %w", err)
}
}
linkNameResolver := network.NewLinkResolver(linkStatuses.All)
var (
specs []network.OperatorSpecSpec
@ -124,7 +93,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
if ctrl.Cmdline != nil {
var settings CmdlineNetworking
settings, err = ParseCmdlineNetwork(ctrl.Cmdline)
settings, err = ParseCmdlineNetwork(ctrl.Cmdline, linkNameResolver)
if err != nil {
logger.Warn("ignored cmdline parse failure", zap.Error(err))
}
@ -140,7 +109,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
specs = append(specs, network.OperatorSpecSpec{
Operator: network.OperatorDHCP4,
LinkName: lookupLinkName(linkConfig.LinkName),
LinkName: linkNameResolver.Resolve(linkConfig.LinkName),
RequireUp: true,
DHCP4: network.DHCP4OperatorSpec{
RouteMetric: network.DefaultRouteMetric,
@ -150,6 +119,13 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
}
}
items, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.DeviceConfigSpecType, "", resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting config: %w", err)
}
}
devices := xslices.Map(items.Items, func(item resource.Resource) talosconfig.Device {
return item.(*network.DeviceConfigSpec).TypedSpec().Device
})
@ -158,10 +134,10 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
if len(devices) > 0 {
for _, device := range devices {
if device.Ignore() {
ignoredInterfaces[device.Interface()] = struct{}{}
ignoredInterfaces[linkNameResolver.Resolve(device.Interface())] = struct{}{}
}
if _, ignore := ignoredInterfaces[device.Interface()]; ignore {
if _, ignore := ignoredInterfaces[linkNameResolver.Resolve(device.Interface())]; ignore {
continue
}
@ -173,7 +149,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
specs = append(specs, network.OperatorSpecSpec{
Operator: network.OperatorDHCP4,
LinkName: lookupLinkName(device.Interface()),
LinkName: linkNameResolver.Resolve(device.Interface()),
RequireUp: true,
DHCP4: network.DHCP4OperatorSpec{
RouteMetric: routeMetric,
@ -190,7 +166,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
specs = append(specs, network.OperatorSpecSpec{
Operator: network.OperatorDHCP6,
LinkName: lookupLinkName(device.Interface()),
LinkName: linkNameResolver.Resolve(device.Interface()),
RequireUp: true,
DHCP6: network.DHCP6OperatorSpec{
RouteMetric: routeMetric,
@ -243,13 +219,13 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
// any link which has any configuration derived from the machine configuration or platform configuration should be ignored
configuredInterfaces := map[string]struct{}{}
list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined))
linkSpecs, err := safe.ReaderList[*network.LinkSpec](ctx, r, resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing link specs: %w", err)
}
for _, item := range list.Items {
linkSpec := item.(*network.LinkSpec).TypedSpec()
for link := range linkSpecs.All() {
linkSpec := link.TypedSpec()
switch linkSpec.ConfigLayer {
case network.ConfigDefault:
@ -258,7 +234,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
// specs produced by operators, ignore
case network.ConfigCmdline, network.ConfigMachineConfiguration, network.ConfigPlatform:
// interface is configured explicitly, don't run default dhcp4
configuredInterfaces[lookupLinkName(linkSpec.Name)] = struct{}{}
configuredInterfaces[linkNameResolver.Resolve(linkSpec.Name)] = struct{}{}
}
}
@ -294,12 +270,12 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
}
// list specs for cleanup
list, err = r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined))
operators, err := safe.ReaderList[*network.OperatorSpec](ctx, r, resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for _, res := range list.Items {
for res := range operators.All() {
if res.Metadata().Owner() != ctrl.Name() {
// skip specs created by other controllers
continue

View File

@ -6,25 +6,18 @@
package network_test
import (
"context"
"net/url"
"sync"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-procfs/procfs"
"github.com/siderolabs/go-retry/retry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"go.uber.org/zap/zaptest"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
netctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network"
"github.com/siderolabs/talos/pkg/machinery/config/container"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
@ -34,86 +27,34 @@ import (
)
type OperatorConfigSuite struct {
suite.Suite
state state.State
runtime *runtime.Runtime
wg sync.WaitGroup
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
func (suite *OperatorConfigSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
var err error
suite.runtime, err = runtime.NewRuntime(suite.state, zaptest.NewLogger(suite.T()))
suite.Require().NoError(err)
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.DeviceConfigController{}))
}
func (suite *OperatorConfigSuite) startRuntime() {
suite.wg.Add(1)
go func() {
defer suite.wg.Done()
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
ctest.DefaultSuite
}
func (suite *OperatorConfigSuite) assertOperators(requiredIDs []string, check func(*network.OperatorSpec, *assert.Assertions)) {
assertResources(suite.ctx, suite.T(), suite.state, requiredIDs, check, rtestutils.WithNamespace(network.ConfigNamespaceName))
ctest.AssertResources(suite, requiredIDs, check, rtestutils.WithNamespace(network.ConfigNamespaceName))
}
func (suite *OperatorConfigSuite) assertNoOperators(unexpectedIDs []string) error {
unexpIDs := make(map[string]struct{}, len(unexpectedIDs))
func (suite *OperatorConfigSuite) assertNoOperators(unexpectedIDs []string) {
for _, id := range unexpectedIDs {
unexpIDs[id] = struct{}{}
ctest.AssertNoResource[*network.OperatorSpec](suite, id, rtestutils.WithNamespace(network.ConfigNamespaceName))
}
resources, err := suite.state.List(
suite.ctx,
resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined),
)
if err != nil {
return err
}
for _, res := range resources.Items {
_, unexpected := unexpIDs[res.Metadata().ID()]
if unexpected {
return retry.ExpectedErrorf("unexpected ID %q", res.Metadata().ID())
}
}
return nil
}
func (suite *OperatorConfigSuite) TestDefaultDHCP() {
suite.Require().NoError(
suite.runtime.RegisterController(
suite.Runtime().RegisterController(
&netctrl.OperatorConfigController{
Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth2"),
},
),
)
suite.startRuntime()
for _, link := range []string{"eth0", "eth1", "eth2"} {
linkStatus := network.NewLinkStatus(network.NamespaceName, link)
linkStatus.TypedSpec().Type = nethelpers.LinkEther
linkStatus.TypedSpec().LinkState = true
suite.Require().NoError(suite.state.Create(suite.ctx, linkStatus))
suite.Create(linkStatus)
}
suite.assertOperators(
@ -137,21 +78,19 @@ func (suite *OperatorConfigSuite) TestDefaultDHCP() {
func (suite *OperatorConfigSuite) TestDefaultDHCPCmdline() {
suite.Require().NoError(
suite.runtime.RegisterController(
suite.Runtime().RegisterController(
&netctrl.OperatorConfigController{
Cmdline: procfs.NewCmdline("ip=172.20.0.2::172.20.0.1:255.255.255.0::eth1::::: ip=eth3:dhcp"),
},
),
)
suite.startRuntime()
for _, link := range []string{"eth0", "eth1", "eth2"} {
linkStatus := network.NewLinkStatus(network.NamespaceName, link)
linkStatus.TypedSpec().Type = nethelpers.LinkEther
linkStatus.TypedSpec().LinkState = true
suite.Require().NoError(suite.state.Create(suite.ctx, linkStatus))
suite.Create(linkStatus)
}
suite.assertOperators(
@ -177,28 +116,22 @@ func (suite *OperatorConfigSuite) TestDefaultDHCPCmdline() {
// remove link
suite.Require().NoError(
suite.state.Destroy(
suite.ctx,
suite.State().Destroy(
suite.Ctx(),
resource.NewMetadata(network.NamespaceName, network.LinkStatusType, "eth2", resource.VersionUndefined),
),
)
suite.Assert().NoError(
retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertNoOperators(
[]string{
"default/dhcp4/eth2",
},
)
},
),
suite.assertNoOperators(
[]string{
"default/dhcp4/eth2",
},
)
}
func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() {
suite.Require().NoError(
suite.runtime.RegisterController(
suite.Runtime().RegisterController(
&netctrl.OperatorConfigController{
Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth5"),
},
@ -206,21 +139,19 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() {
)
// add LinkConfig controller to produce link specs based on machine configuration
suite.Require().NoError(
suite.runtime.RegisterController(
suite.Runtime().RegisterController(
&netctrl.LinkConfigController{
Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth5"),
},
),
)
suite.startRuntime()
for _, link := range []string{"eth0", "eth1", "eth2"} {
linkStatus := network.NewLinkStatus(network.NamespaceName, link)
linkStatus.TypedSpec().Type = nethelpers.LinkEther
linkStatus.TypedSpec().LinkState = true
suite.Require().NoError(suite.state.Create(suite.ctx, linkStatus))
suite.Create(linkStatus)
}
u, err := url.Parse("https://foo:6443")
@ -289,7 +220,7 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() {
),
)
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
suite.Create(cfg)
suite.assertOperators(
[]string{
@ -320,27 +251,19 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() {
},
)
suite.Assert().NoError(
retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertNoOperators(
[]string{
"configuration/dhcp4/eth0",
"default/dhcp4/eth0",
"configuration/dhcp4/eth2",
"default/dhcp4/eth2",
"configuration/dhcp4/eth4.26",
},
)
},
),
suite.assertNoOperators(
[]string{
"configuration/dhcp4/eth0",
"default/dhcp4/eth0",
"configuration/dhcp4/eth2",
"default/dhcp4/eth2",
"configuration/dhcp4/eth4.26",
},
)
}
func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP6() {
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.OperatorConfigController{}))
suite.startRuntime()
suite.Require().NoError(suite.Runtime().RegisterController(&netctrl.OperatorConfigController{}))
u, err := url.Parse("https://foo:6443")
suite.Require().NoError(err)
@ -388,7 +311,7 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP6() {
),
)
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
suite.Create(cfg)
suite.assertOperators(
[]string{
@ -409,27 +332,170 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP6() {
},
)
suite.Assert().NoError(
retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertNoOperators(
[]string{
"configuration/dhcp6/eth1",
},
)
},
),
suite.assertNoOperators(
[]string{
"configuration/dhcp6/eth1",
},
)
}
func (suite *OperatorConfigSuite) TearDownTest() {
suite.T().Log("tear down")
func (suite *OperatorConfigSuite) TestMachineConfigurationWithAliases() {
suite.Require().NoError(
suite.Runtime().RegisterController(
&netctrl.OperatorConfigController{},
),
)
// add LinkConfig controller to produce link specs based on machine configuration
suite.Require().NoError(
suite.Runtime().RegisterController(
&netctrl.LinkConfigController{},
),
)
suite.ctxCancel()
for _, link := range []struct {
name string
aliases []string
}{
{
name: "eth0",
aliases: []string{"enx0123"},
},
{
name: "eth1",
aliases: []string{"enx0456"},
},
{
name: "eth2",
aliases: []string{"enxa"},
},
{
name: "eth3",
aliases: []string{"enxb"},
},
{
name: "eth4",
aliases: []string{"enxc"},
},
} {
status := network.NewLinkStatus(network.NamespaceName, link.name)
status.TypedSpec().AltNames = link.aliases
status.TypedSpec().Type = nethelpers.LinkEther
status.TypedSpec().LinkState = true
suite.wg.Wait()
suite.Create(status)
}
u, err := url.Parse("https://foo:6443")
suite.Require().NoError(err)
cfg := config.NewMachineConfig(
container.NewV1Alpha1(
&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
NetworkInterfaces: []*v1alpha1.Device{
{
DeviceInterface: "enx0123",
},
{
DeviceInterface: "enx0456",
DeviceDHCP: pointer.To(true),
},
{
DeviceIgnore: pointer.To(true),
DeviceInterface: "enxa",
DeviceDHCP: pointer.To(true),
},
{
DeviceInterface: "enxb",
DeviceDHCP: pointer.To(true),
DeviceDHCPOptions: &v1alpha1.DHCPOptions{
DHCPIPv4: pointer.To(true),
DHCPRouteMetric: 256,
},
},
{
DeviceInterface: "enxc",
DeviceVlans: []*v1alpha1.Vlan{
{
VlanID: 25,
VlanDHCP: pointer.To(true),
},
{
VlanID: 26,
},
{
VlanID: 27,
VlanDHCPOptions: &v1alpha1.DHCPOptions{
DHCPRouteMetric: 256,
},
},
},
},
{
DeviceInterface: "enxd",
DeviceDHCP: pointer.To(true),
},
},
},
},
ClusterConfig: &v1alpha1.ClusterConfig{
ControlPlane: &v1alpha1.ControlPlaneConfig{
Endpoint: &v1alpha1.Endpoint{
URL: u,
},
},
},
},
),
)
suite.Create(cfg)
suite.assertOperators(
[]string{
"configuration/dhcp4/eth1",
"configuration/dhcp4/eth3",
"configuration/dhcp4/enxc.25",
}, func(r *network.OperatorSpec, asrt *assert.Assertions) {
asrt.Equal(network.OperatorDHCP4, r.TypedSpec().Operator)
asrt.True(r.TypedSpec().RequireUp)
switch r.Metadata().ID() {
case "configuration/dhcp4/eth1":
asrt.Equal("eth1", r.TypedSpec().LinkName)
asrt.EqualValues(network.DefaultRouteMetric, r.TypedSpec().DHCP4.RouteMetric)
case "configuration/dhcp4/eth3":
asrt.Equal("eth3", r.TypedSpec().LinkName)
asrt.EqualValues(256, r.TypedSpec().DHCP4.RouteMetric)
case "configuration/dhcp4/enxc.25":
asrt.Equal("enxc.25", r.TypedSpec().LinkName)
asrt.EqualValues(network.DefaultRouteMetric, r.TypedSpec().DHCP4.RouteMetric)
}
},
)
suite.assertNoOperators(
[]string{
"configuration/dhcp4/eth0",
"default/dhcp4/eth0",
"configuration/dhcp4/eth2",
"default/dhcp4/eth2",
"configuration/dhcp4/eth4.26",
},
)
}
func TestOperatorConfigSuite(t *testing.T) {
suite.Run(t, new(OperatorConfigSuite))
t.Parallel()
suite.Run(t, &OperatorConfigSuite{
DefaultSuite: ctest.DefaultSuite{
Timeout: 5 * time.Second,
AfterSetup: func(s *ctest.DefaultSuite) {
s.Require().NoError(s.Runtime().RegisterController(&netctrl.DeviceConfigController{}))
},
},
})
}

View File

@ -42,6 +42,11 @@ func (ctrl *OperatorVIPConfigController) Inputs() []controller.Input {
Type: network.DeviceConfigSpecType,
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.LinkStatusType,
Kind: controller.InputWeak,
},
}
}
@ -79,12 +84,19 @@ func (ctrl *OperatorVIPConfigController) Run(ctx context.Context, r controller.R
return item.(*network.DeviceConfigSpec).TypedSpec().Device
})
linkStatuses, err := safe.ReaderListAll[*network.LinkStatus](ctx, r)
if err != nil {
return fmt.Errorf("error listing link statuses: %w", err)
}
linkNameResolver := network.NewLinkResolver(linkStatuses.All)
ignoredInterfaces := map[string]struct{}{}
if ctrl.Cmdline != nil {
var settings CmdlineNetworking
settings, err = ParseCmdlineNetwork(ctrl.Cmdline)
settings, err = ParseCmdlineNetwork(ctrl.Cmdline, linkNameResolver)
if err != nil {
logger.Warn("ignored cmdline parse failure", zap.Error(err))
}
@ -103,15 +115,15 @@ func (ctrl *OperatorVIPConfigController) Run(ctx context.Context, r controller.R
if len(devices) > 0 {
for _, device := range devices {
if device.Ignore() {
ignoredInterfaces[device.Interface()] = struct{}{}
ignoredInterfaces[linkNameResolver.Resolve(device.Interface())] = struct{}{}
}
if _, ignore := ignoredInterfaces[device.Interface()]; ignore {
if _, ignore := ignoredInterfaces[linkNameResolver.Resolve(device.Interface())]; ignore {
continue
}
if device.VIPConfig() != nil {
if spec, specErr := handleVIP(ctx, device.VIPConfig(), device.Interface(), logger); specErr != nil {
if spec, specErr := handleVIP(ctx, device.VIPConfig(), linkNameResolver.Resolve(device.Interface()), logger); specErr != nil {
specErrors = multierror.Append(specErrors, specErr)
} else {
specs = append(specs, spec)

View File

@ -5,23 +5,17 @@
package network_test
import (
"context"
"net/netip"
"net/url"
"sync"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/siderolabs/go-pointer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"go.uber.org/zap/zaptest"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
netctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network"
"github.com/siderolabs/talos/pkg/machinery/config/container"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
@ -30,51 +24,35 @@ import (
)
type OperatorVIPConfigSuite struct {
suite.Suite
state state.State
runtime *runtime.Runtime
wg sync.WaitGroup
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
func (suite *OperatorVIPConfigSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
var err error
suite.runtime, err = runtime.NewRuntime(suite.state, zaptest.NewLogger(suite.T()))
suite.Require().NoError(err)
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.DeviceConfigController{}))
}
func (suite *OperatorVIPConfigSuite) startRuntime() {
suite.wg.Add(1)
go func() {
defer suite.wg.Done()
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
ctest.DefaultSuite
}
func (suite *OperatorVIPConfigSuite) assertOperators(
requiredIDs []string,
check func(*network.OperatorSpec, *assert.Assertions),
) {
assertResources(suite.ctx, suite.T(), suite.state, requiredIDs, check, rtestutils.WithNamespace(network.ConfigNamespaceName))
ctest.AssertResources(suite, requiredIDs, check, rtestutils.WithNamespace(network.ConfigNamespaceName))
}
func (suite *OperatorVIPConfigSuite) TestMachineConfigurationVIP() {
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.OperatorVIPConfigController{}))
for _, link := range []struct {
name string
aliases []string
}{
{
name: "eth5",
aliases: []string{"enxa"},
},
{
name: "eth6",
aliases: []string{"enxb"},
},
} {
status := network.NewLinkStatus(network.NamespaceName, link.name)
status.TypedSpec().AltNames = link.aliases
suite.startRuntime()
suite.Create(status)
}
u, err := url.Parse("https://foo:6443")
suite.Require().NoError(err)
@ -112,6 +90,13 @@ func (suite *OperatorVIPConfigSuite) TestMachineConfigurationVIP() {
},
},
},
{
DeviceInterface: "enxa",
DeviceDHCP: pointer.To(true),
DeviceVIPConfig: &v1alpha1.DeviceVIPConfig{
SharedIP: "2.3.4.5",
},
},
},
},
},
@ -126,13 +111,14 @@ func (suite *OperatorVIPConfigSuite) TestMachineConfigurationVIP() {
),
)
suite.Require().NoError(suite.state.Create(suite.ctx, cfg))
suite.Create(cfg)
suite.assertOperators(
[]string{
"configuration/vip/eth1",
"configuration/vip/eth2",
"configuration/vip/eth3.26",
"configuration/vip/eth5",
}, func(r *network.OperatorSpec, asrt *assert.Assertions) {
asrt.Equal(network.OperatorVIP, r.TypedSpec().Operator)
asrt.True(r.TypedSpec().RequireUp)
@ -141,6 +127,9 @@ func (suite *OperatorVIPConfigSuite) TestMachineConfigurationVIP() {
case "configuration/vip/eth1":
asrt.Equal("eth1", r.TypedSpec().LinkName)
asrt.EqualValues(netip.MustParseAddr("2.3.4.5"), r.TypedSpec().VIP.IP)
case "configuration/vip/eth5":
asrt.Equal("eth5", r.TypedSpec().LinkName)
asrt.EqualValues(netip.MustParseAddr("2.3.4.5"), r.TypedSpec().VIP.IP)
case "configuration/vip/eth2":
asrt.Equal("eth2", r.TypedSpec().LinkName)
asrt.EqualValues(
@ -155,14 +144,16 @@ func (suite *OperatorVIPConfigSuite) TestMachineConfigurationVIP() {
)
}
func (suite *OperatorVIPConfigSuite) TearDownTest() {
suite.T().Log("tear down")
suite.ctxCancel()
suite.wg.Wait()
}
func TestOperatorVIPConfigSuite(t *testing.T) {
suite.Run(t, new(OperatorVIPConfigSuite))
t.Parallel()
suite.Run(t, &OperatorVIPConfigSuite{
DefaultSuite: ctest.DefaultSuite{
Timeout: 5 * time.Second,
AfterSetup: func(s *ctest.DefaultSuite) {
s.Require().NoError(s.Runtime().RegisterController(&netctrl.DeviceConfigController{}))
s.Require().NoError(s.Runtime().RegisterController(&netctrl.OperatorVIPConfigController{}))
},
},
})
}

View File

@ -198,7 +198,7 @@ func (ctrl *ResolverConfigController) parseCmdline(logger *zap.Logger) (spec net
return
}
settings, err := ParseCmdlineNetwork(ctrl.Cmdline)
settings, err := ParseCmdlineNetwork(ctrl.Cmdline, network.NewEmptyLinkResolver())
if err != nil {
logger.Warn("ignoring error", zap.Error(err))

View File

@ -181,7 +181,7 @@ func (ctrl *RouteConfigController) parseCmdline(logger *zap.Logger) (routes []ne
return
}
settings, err := ParseCmdlineNetwork(ctrl.Cmdline)
settings, err := ParseCmdlineNetwork(ctrl.Cmdline, network.NewEmptyLinkResolver())
if err != nil {
logger.Info("ignoring error", zap.Error(err))

View File

@ -171,7 +171,7 @@ func (ctrl *TimeServerConfigController) parseCmdline(logger *zap.Logger) (spec n
return
}
settings, err := ParseCmdlineNetwork(ctrl.Cmdline)
settings, err := ParseCmdlineNetwork(ctrl.Cmdline, network.NewEmptyLinkResolver())
if err != nil {
logger.Warn("ignoring error", zap.Error(err))

View File

@ -0,0 +1,45 @@
// 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/.
package network
import "iter"
// LinkResolver resolves link names and aliases to actual link names.
type LinkResolver struct {
lookup map[string]string // map of link names/aliases to link names
}
// Resolve resolves the link name or alias to the actual link name.
//
// If the link name or alias is not found in the lookup table, it is returned as is.
func (r *LinkResolver) Resolve(name string) string {
if resolved, ok := r.lookup[name]; ok {
return resolved
}
return name
}
// NewLinkResolver creates a new link name resolver.
func NewLinkResolver(f func() iter.Seq[*LinkStatus]) *LinkResolver {
lookup := make(map[string]string)
for link := range f() {
for alias := range AllLinkAliases(link) {
lookup[alias] = link.Metadata().ID()
}
}
for link := range f() {
lookup[link.Metadata().ID()] = link.Metadata().ID()
}
return &LinkResolver{lookup: lookup}
}
// NewEmptyLinkResolver creates a new link name resolver with an empty lookup table.
func NewEmptyLinkResolver() *LinkResolver {
return &LinkResolver{}
}

View File

@ -0,0 +1,44 @@
// 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/.
package network_test
import (
"iter"
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
)
func TestLinkNameResolver(t *testing.T) {
t.Parallel()
link1 := network.NewLinkStatus(network.NamespaceName, "eth0")
link1.TypedSpec().Alias = "net0"
link1.TypedSpec().AltNames = []string{"ext0"}
link2 := network.NewLinkStatus(network.NamespaceName, "eth1")
link3 := network.NewLinkStatus(network.NamespaceName, "eth2")
link3.TypedSpec().AltNames = []string{"ext2"}
links := []*network.LinkStatus{
link1,
link2,
link3,
}
resolver := network.NewLinkResolver(func() iter.Seq[*network.LinkStatus] { return slices.Values(links) })
assert.Equal(t, "eth0", resolver.Resolve("eth0"))
assert.Equal(t, "eth0", resolver.Resolve("net0"))
assert.Equal(t, "eth0", resolver.Resolve("ext0"))
assert.Equal(t, "eth1", resolver.Resolve("eth1"))
assert.Equal(t, "eth2", resolver.Resolve("eth2"))
assert.Equal(t, "eth2", resolver.Resolve("ext2"))
assert.Equal(t, "eth3", resolver.Resolve("eth3"))
}

View File

@ -5,6 +5,8 @@
package network
import (
"iter"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/cosi-project/runtime/pkg/resource/protobuf"
@ -66,6 +68,38 @@ func (s LinkStatusSpec) Physical() bool {
return s.Type == nethelpers.LinkEther && s.Kind == ""
}
// AllLinkNames returns all link names, including name, alias and altnames.
func AllLinkNames(link *LinkStatus) iter.Seq[string] {
return func(yield func(string) bool) {
if !yield(link.Metadata().ID()) {
return
}
for alias := range AllLinkAliases(link) {
if !yield(alias) {
return
}
}
}
}
// AllLinkAliases returns all link aliases (altnames and alias).
func AllLinkAliases(link *LinkStatus) iter.Seq[string] {
return func(yield func(string) bool) {
if link.TypedSpec().Alias != "" {
if !yield(link.TypedSpec().Alias) {
return
}
}
for _, altName := range link.TypedSpec().AltNames {
if !yield(altName) {
return
}
}
}
}
// NewLinkStatus initializes a LinkStatus resource.
func NewLinkStatus(namespace resource.Namespace, id resource.ID) *LinkStatus {
return typed.NewResource[LinkStatusSpec, LinkStatusExtension](