diff --git a/internal/app/machined/pkg/controllers/network/address_config.go b/internal/app/machined/pkg/controllers/network/address_config.go index 912ac9944..7826a8903 100644 --- a/internal/app/machined/pkg/controllers/network/address_config.go +++ b/internal/app/machined/pkg/controllers/network/address_config.go @@ -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)) diff --git a/internal/app/machined/pkg/controllers/network/cmdline.go b/internal/app/machined/pkg/controllers/network/cmdline.go index 02d572ce9..2a6c55d5a 100644 --- a/internal/app/machined/pkg/controllers/network/cmdline.go +++ b/internal/app/machined/pkg/controllers/network/cmdline.go @@ -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=: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=: 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]) diff --git a/internal/app/machined/pkg/controllers/network/cmdline_test.go b/internal/app/machined/pkg/controllers/network/cmdline_test.go index 3b5369240..eb0fda15a 100644 --- a/internal/app/machined/pkg/controllers/network/cmdline_test.go +++ b/internal/app/machined/pkg/controllers/network/cmdline_test.go @@ -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)) -} diff --git a/internal/app/machined/pkg/controllers/network/dns_resolve_cache_test.go b/internal/app/machined/pkg/controllers/network/dns_resolve_cache_test.go index bc7165770..b1db52bf3 100644 --- a/internal/app/machined/pkg/controllers/network/dns_resolve_cache_test.go +++ b/internal/app/machined/pkg/controllers/network/dns_resolve_cache_test.go @@ -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 diff --git a/internal/app/machined/pkg/controllers/network/hostname_config.go b/internal/app/machined/pkg/controllers/network/hostname_config.go index 8c6c73630..7d24b216e 100644 --- a/internal/app/machined/pkg/controllers/network/hostname_config.go +++ b/internal/app/machined/pkg/controllers/network/hostname_config.go @@ -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)) diff --git a/internal/app/machined/pkg/controllers/network/link_config.go b/internal/app/machined/pkg/controllers/network/link_config.go index 674127d1c..8e1eeaf63 100644 --- a/internal/app/machined/pkg/controllers/network/link_config.go +++ b/internal/app/machined/pkg/controllers/network/link_config.go @@ -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() diff --git a/internal/app/machined/pkg/controllers/network/link_config_test.go b/internal/app/machined/pkg/controllers/network/link_config_test.go index 1ce549e82..949d7ee11 100644 --- a/internal/app/machined/pkg/controllers/network/link_config_test.go +++ b/internal/app/machined/pkg/controllers/network/link_config_test.go @@ -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{})) + }, + }, + }) } diff --git a/internal/app/machined/pkg/controllers/network/operator_config.go b/internal/app/machined/pkg/controllers/network/operator_config.go index b580ed0bb..e0a841bee 100644 --- a/internal/app/machined/pkg/controllers/network/operator_config.go +++ b/internal/app/machined/pkg/controllers/network/operator_config.go @@ -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 diff --git a/internal/app/machined/pkg/controllers/network/operator_config_test.go b/internal/app/machined/pkg/controllers/network/operator_config_test.go index 0493d44ba..3d7e8741a 100644 --- a/internal/app/machined/pkg/controllers/network/operator_config_test.go +++ b/internal/app/machined/pkg/controllers/network/operator_config_test.go @@ -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{})) + }, + }, + }) } diff --git a/internal/app/machined/pkg/controllers/network/operator_vip_config.go b/internal/app/machined/pkg/controllers/network/operator_vip_config.go index 470556e9c..12f28ee41 100644 --- a/internal/app/machined/pkg/controllers/network/operator_vip_config.go +++ b/internal/app/machined/pkg/controllers/network/operator_vip_config.go @@ -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) diff --git a/internal/app/machined/pkg/controllers/network/operator_vip_config_test.go b/internal/app/machined/pkg/controllers/network/operator_vip_config_test.go index ff81bc258..56d17c46e 100644 --- a/internal/app/machined/pkg/controllers/network/operator_vip_config_test.go +++ b/internal/app/machined/pkg/controllers/network/operator_vip_config_test.go @@ -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{})) + }, + }, + }) } diff --git a/internal/app/machined/pkg/controllers/network/resolver_config.go b/internal/app/machined/pkg/controllers/network/resolver_config.go index 93af73740..65bb2e4de 100644 --- a/internal/app/machined/pkg/controllers/network/resolver_config.go +++ b/internal/app/machined/pkg/controllers/network/resolver_config.go @@ -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)) diff --git a/internal/app/machined/pkg/controllers/network/route_config.go b/internal/app/machined/pkg/controllers/network/route_config.go index 0391d30d6..923e1600e 100644 --- a/internal/app/machined/pkg/controllers/network/route_config.go +++ b/internal/app/machined/pkg/controllers/network/route_config.go @@ -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)) diff --git a/internal/app/machined/pkg/controllers/network/timeserver_config.go b/internal/app/machined/pkg/controllers/network/timeserver_config.go index bcf3dcae6..a851b1708 100644 --- a/internal/app/machined/pkg/controllers/network/timeserver_config.go +++ b/internal/app/machined/pkg/controllers/network/timeserver_config.go @@ -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)) diff --git a/pkg/machinery/resources/network/link_name_resolver.go b/pkg/machinery/resources/network/link_name_resolver.go new file mode 100644 index 000000000..a6ab6d557 --- /dev/null +++ b/pkg/machinery/resources/network/link_name_resolver.go @@ -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{} +} diff --git a/pkg/machinery/resources/network/link_name_resolver_test.go b/pkg/machinery/resources/network/link_name_resolver_test.go new file mode 100644 index 000000000..0f2e60dbd --- /dev/null +++ b/pkg/machinery/resources/network/link_name_resolver_test.go @@ -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")) +} diff --git a/pkg/machinery/resources/network/link_status.go b/pkg/machinery/resources/network/link_status.go index ad8e025f3..d423f1bdc 100644 --- a/pkg/machinery/resources/network/link_status.go +++ b/pkg/machinery/resources/network/link_status.go @@ -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](