diff --git a/internal/app/machined/pkg/controllers/network/link_config.go b/internal/app/machined/pkg/controllers/network/link_config.go index 01e977007..37b9fdc1c 100644 --- a/internal/app/machined/pkg/controllers/network/link_config.go +++ b/internal/app/machined/pkg/controllers/network/link_config.go @@ -42,6 +42,11 @@ func (ctrl *LinkConfigController) Inputs() []controller.Input { ID: pointer.ToString(config.V1Alpha1ID), Kind: controller.InputWeak, }, + { + Namespace: network.NamespaceName, + Type: network.LinkStatusType, + Kind: controller.InputWeak, + }, } } @@ -111,7 +116,7 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime, } // parse kernel cmdline for the interface name - cmdlineLink := ctrl.parseCmdline(logger) + cmdlineLink, cmdlineIgnored := ctrl.parseCmdline(logger) if cmdlineLink.Name != "" { if _, ignored := ignoredInterfaces[cmdlineLink.Name]; !ignored { var ids []string @@ -127,7 +132,7 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime, } } - // parse machine configuration for static routes + // parse machine configuration for link specs if cfgProvider != nil { links := ctrl.parseMachineConfiguration(logger, cfgProvider) @@ -143,8 +148,56 @@ func (ctrl *LinkConfigController) Run(ctx context.Context, r controller.Runtime, } } - // list link for cleanup - list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined)) + // bring up any physical link not mentioned explicitly in the machine configuration + configuredLinks := map[string]struct{}{} + + for _, linkName := range cmdlineIgnored { + configuredLinks[linkName] = struct{}{} + } + + if cmdlineLink.Name != "" { + configuredLinks[cmdlineLink.Name] = struct{}{} + } + + if cfgProvider != nil { + for _, device := range cfgProvider.Machine().Network().Devices() { + configuredLinks[device.Interface()] = struct{}{} + } + } + + 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) + } + + for _, item := range list.Items { + linkStatus := item.(*network.LinkStatus) //nolint:errcheck,forcetypeassert + + if _, configured := configuredLinks[linkStatus.Metadata().ID()]; !configured { + if linkStatus.Physical() { + var ids []string + + ids, err = ctrl.apply(ctx, r, []network.LinkSpecSpec{ + { + Name: linkStatus.Metadata().ID(), + Up: true, + ConfigLayer: network.ConfigDefault, + }, + }) + + if err != nil { + return fmt.Errorf("error applying default link up: %w", err) + } + + for _, id := range ids { + touchedIDs[id] = struct{}{} + } + } + } + } + + // list links for cleanup + list, err = r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.LinkSpecType, "", resource.VersionUndefined)) if err != nil { return fmt.Errorf("error listing resources: %w", err) } @@ -189,23 +242,23 @@ func (ctrl *LinkConfigController) apply(ctx context.Context, r controller.Runtim return ids, nil } -func (ctrl *LinkConfigController) parseCmdline(logger *zap.Logger) network.LinkSpecSpec { +func (ctrl *LinkConfigController) parseCmdline(logger *zap.Logger) (network.LinkSpecSpec, []string) { if ctrl.Cmdline == nil { - return network.LinkSpecSpec{} + return network.LinkSpecSpec{}, nil } settings, err := ParseCmdlineNetwork(ctrl.Cmdline) if err != nil { logger.Info("ignoring error", zap.Error(err)) - return network.LinkSpecSpec{} + return network.LinkSpecSpec{}, nil } return network.LinkSpecSpec{ Name: settings.LinkName, Up: true, ConfigLayer: network.ConfigCmdline, - } + }, settings.IgnoreInterfaces } //nolint:gocyclo 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 559a65782..a53f5adbf 100644 --- a/internal/app/machined/pkg/controllers/network/link_config_test.go +++ b/internal/app/machined/pkg/controllers/network/link_config_test.go @@ -97,6 +97,28 @@ func (suite *LinkConfigSuite) assertLinks(requiredIDs []string, check func(*netw return nil } +func (suite *LinkConfigSuite) assertNoLinks(unexpectedIDs []string) error { + unexpIDs := make(map[string]struct{}, len(unexpectedIDs)) + + for _, id := range unexpectedIDs { + unexpIDs[id] = struct{}{} + } + + 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{})) @@ -289,6 +311,77 @@ func (suite *LinkConfigSuite) TestMachineConfiguration() { })) } +func (suite *LinkConfigSuite) TestDefaultUp() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.LinkConfigController{ + Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth2"), + })) + + 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)) + } + + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig(&v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineNetwork: &v1alpha1.NetworkConfig{ + NetworkInterfaces: []*v1alpha1.Device{ + { + DeviceInterface: "eth0", + DeviceVlans: []*v1alpha1.Vlan{ + { + VlanID: 24, + VlanCIDR: "10.0.0.1/8", + }, + { + VlanID: 48, + VlanCIDR: "10.0.0.2/8", + }, + }, + }, + }, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: u, + }, + }, + }, + }) + + suite.Require().NoError(suite.state.Create(suite.ctx, cfg)) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertLinks([]string{ + "default/eth1", + }, func(r *network.LinkSpec) error { + suite.Assert().Equal(network.ConfigDefault, r.TypedSpec().ConfigLayer) + suite.Assert().True(r.TypedSpec().Up) + + return nil + }) + })) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertNoLinks([]string{ + "default/eth0", + "default/eth2", + }) + })) +} + func TestLinkConfigSuite(t *testing.T) { suite.Run(t, new(LinkConfigSuite)) }