diff --git a/internal/app/machined/pkg/controllers/network/address_config.go b/internal/app/machined/pkg/controllers/network/address_config.go index 9c5d59a9b..0d5cd3224 100644 --- a/internal/app/machined/pkg/controllers/network/address_config.go +++ b/internal/app/machined/pkg/controllers/network/address_config.go @@ -155,6 +155,7 @@ func (ctrl *AddressConfigController) Run(ctx context.Context, r controller.Runti } } +//nolint:dupl func (ctrl *AddressConfigController) apply(ctx context.Context, r controller.Runtime, addresses []network.AddressSpecSpec) ([]resource.ID, error) { ids := make([]string, 0, len(addresses)) diff --git a/internal/app/machined/pkg/controllers/network/address_merge.go b/internal/app/machined/pkg/controllers/network/address_merge.go index fada4623c..5974ee122 100644 --- a/internal/app/machined/pkg/controllers/network/address_merge.go +++ b/internal/app/machined/pkg/controllers/network/address_merge.go @@ -3,6 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package network provides controllers which manage network resources. +// +//nolint:dupl package network import ( diff --git a/internal/app/machined/pkg/controllers/network/address_merge_test.go b/internal/app/machined/pkg/controllers/network/address_merge_test.go index 52654894c..763d82259 100644 --- a/internal/app/machined/pkg/controllers/network/address_merge_test.go +++ b/internal/app/machined/pkg/controllers/network/address_merge_test.go @@ -99,7 +99,7 @@ func (suite *AddressMergeSuite) assertAddresses(requiredIDs []string, check func } func (suite *AddressMergeSuite) assertNoAddress(id string) error { - resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.AddressStatusType, "", resource.VersionUndefined)) + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.AddressSpecType, "", resource.VersionUndefined)) if err != nil { return err } diff --git a/internal/app/machined/pkg/controllers/network/operator_config.go b/internal/app/machined/pkg/controllers/network/operator_config.go index f1df5b716..98d106e16 100644 --- a/internal/app/machined/pkg/controllers/network/operator_config.go +++ b/internal/app/machined/pkg/controllers/network/operator_config.go @@ -47,6 +47,11 @@ func (ctrl *OperatorConfigController) Inputs() []controller.Input { Type: network.LinkStatusType, Kind: controller.InputWeak, }, + { + Namespace: network.ConfigNamespaceName, + Type: network.LinkSpecType, + Kind: controller.InputWeak, + }, } } @@ -55,7 +60,7 @@ func (ctrl *OperatorConfigController) Outputs() []controller.Output { return []controller.Output{ { Type: network.OperatorSpecType, - Kind: controller.OutputExclusive, + Kind: controller.OutputShared, }, } } @@ -85,7 +90,6 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt } ignoredInterfaces := map[string]struct{}{} - configuredInterfaces := map[string]struct{}{} if ctrl.Cmdline != nil { var settings CmdlineNetworking @@ -98,10 +102,6 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt for _, link := range settings.IgnoreInterfaces { ignoredInterfaces[link] = struct{}{} } - - if settings.LinkName != "" { - configuredInterfaces[settings.LinkName] = struct{}{} - } } var ( @@ -112,22 +112,14 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt // operators from the config if cfgProvider != nil { for _, device := range cfgProvider.Machine().Network().Devices() { - configuredInterfaces[device.Interface()] = struct{}{} - if device.Ignore() { - continue + ignoredInterfaces[device.Interface()] = struct{}{} } if _, ignore := ignoredInterfaces[device.Interface()]; ignore { continue } - if device.Bond() != nil { - for _, link := range device.Bond().Interfaces() { - configuredInterfaces[link] = struct{}{} - } - } - if device.DHCP() && device.DHCPOptions().IPv4() { routeMetric := device.DHCPOptions().RouteMetric() if routeMetric == 0 { @@ -141,6 +133,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt DHCP4: network.DHCP4OperatorSpec{ RouteMetric: routeMetric, }, + ConfigLayer: network.ConfigMachineConfiguration, }) } @@ -157,6 +150,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt DHCP6: network.DHCP6OperatorSpec{ RouteMetric: routeMetric, }, + ConfigLayer: network.ConfigMachineConfiguration, }) } @@ -177,6 +171,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt DHCP4: network.DHCP4OperatorSpec{ RouteMetric: DefaultRouteMetric, }, + ConfigLayer: network.ConfigMachineConfiguration, }) } @@ -192,10 +187,33 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt } } - // operators from defaults - list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.LinkStatusType, "", resource.VersionUndefined)) + // build configuredInterfaces from linkSpecs in `network-config` namespace + // 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)) if err != nil { - return fmt.Errorf("error listing link statuses") + return fmt.Errorf("error listing link specs: %w", err) + } + + for _, item := range list.Items { + linkSpec := item.(*network.LinkSpec).TypedSpec() + + switch linkSpec.ConfigLayer { + case network.ConfigDefault: + // ignore default link specs + case network.ConfigOperator: + // specs produced by operators, ignore + case network.ConfigCmdline, network.ConfigMachineConfiguration, network.ConfigPlatform: + // interface is configured explicitly, don't run default dhcp4 + configuredInterfaces[linkSpec.Name] = struct{}{} + } + } + + // operators from defaults + 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 { @@ -212,6 +230,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt DHCP4: network.DHCP4OperatorSpec{ RouteMetric: DefaultRouteMetric, }, + ConfigLayer: network.ConfigDefault, }) } } @@ -230,7 +249,7 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt } // list specs for cleanup - list, err = r.List(ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + list, err = r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) if err != nil { return fmt.Errorf("error listing resources: %w", err) } @@ -261,11 +280,11 @@ func (ctrl *OperatorConfigController) apply(ctx context.Context, r controller.Ru for _, spec := range specs { spec := spec - id := network.OperatorID(spec.Operator, spec.LinkName) + id := network.LayeredID(spec.ConfigLayer, network.OperatorID(spec.Operator, spec.LinkName)) if err := r.Modify( ctx, - network.NewOperatorSpec(network.NamespaceName, id), + network.NewOperatorSpec(network.ConfigNamespaceName, id), func(r resource.Resource) error { *r.(*network.OperatorSpec).TypedSpec() = spec @@ -299,6 +318,7 @@ func handleVIP(ctx context.Context, vlanConfig talosconfig.VIPConfig, deviceName IP: sharedIP, GratuitousARP: true, }, + ConfigLayer: network.ConfigMachineConfiguration, } switch { 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 f00b9262a..0630ca091 100644 --- a/internal/app/machined/pkg/controllers/network/operator_config_test.go +++ b/internal/app/machined/pkg/controllers/network/operator_config_test.go @@ -73,7 +73,7 @@ func (suite *OperatorConfigSuite) assertOperators(requiredIDs []string, check fu missingIDs[id] = struct{}{} } - resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) if err != nil { return err } @@ -105,7 +105,7 @@ func (suite *OperatorConfigSuite) assertNoOperators(unexpectedIDs []string) erro unexpIDs[id] = struct{}{} } - resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) if err != nil { return err } @@ -138,17 +138,17 @@ func (suite *OperatorConfigSuite) TestDefaultDHCP() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertOperators([]string{ - "dhcp4/eth0", - "dhcp4/eth1", + "default/dhcp4/eth0", + "default/dhcp4/eth1", }, func(r *network.OperatorSpec) error { suite.Assert().Equal(network.OperatorDHCP4, r.TypedSpec().Operator) suite.Assert().True(r.TypedSpec().RequireUp) suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.TypedSpec().DHCP4.RouteMetric) switch r.Metadata().ID() { - case "dhcp4/eth0": + case "default/dhcp4/eth0": suite.Assert().Equal("eth0", r.TypedSpec().LinkName) - case "dhcp4/eth1": + case "default/dhcp4/eth1": suite.Assert().Equal("eth1", r.TypedSpec().LinkName) } @@ -175,17 +175,17 @@ func (suite *OperatorConfigSuite) TestDefaultDHCPCmdline() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertOperators([]string{ - "dhcp4/eth0", - "dhcp4/eth2", + "default/dhcp4/eth0", + "default/dhcp4/eth2", }, func(r *network.OperatorSpec) error { suite.Assert().Equal(network.OperatorDHCP4, r.TypedSpec().Operator) suite.Assert().True(r.TypedSpec().RequireUp) suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.TypedSpec().DHCP4.RouteMetric) switch r.Metadata().ID() { - case "dhcp4/eth0": + case "default/dhcp4/eth0": suite.Assert().Equal("eth0", r.TypedSpec().LinkName) - case "dhcp4/eth2": + case "default/dhcp4/eth2": suite.Assert().Equal("eth2", r.TypedSpec().LinkName) } @@ -199,7 +199,7 @@ func (suite *OperatorConfigSuite) TestDefaultDHCPCmdline() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertNoOperators([]string{ - "dhcp4/eth2", + "default/dhcp4/eth2", }) })) } @@ -208,6 +208,10 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() { suite.Require().NoError(suite.runtime.RegisterController(&netctrl.OperatorConfigController{ Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth5"), })) + // add LinkConfig controller to produce link specs based on machine configuration + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.LinkConfigController{ + Cmdline: procfs.NewCmdline("talos.network.interface.ignore=eth5"), + })) suite.startRuntime() @@ -280,21 +284,21 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertOperators([]string{ - "dhcp4/eth1", - "dhcp4/eth3", - "dhcp4/eth4.25", + "configuration/dhcp4/eth1", + "configuration/dhcp4/eth3", + "configuration/dhcp4/eth4.25", }, func(r *network.OperatorSpec) error { suite.Assert().Equal(network.OperatorDHCP4, r.TypedSpec().Operator) suite.Assert().True(r.TypedSpec().RequireUp) switch r.Metadata().ID() { - case "dhcp4/eth1": + case "configuration/dhcp4/eth1": suite.Assert().Equal("eth1", r.TypedSpec().LinkName) suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.TypedSpec().DHCP4.RouteMetric) - case "dhcp4/eth3": + case "configuration/dhcp4/eth3": suite.Assert().Equal("eth3", r.TypedSpec().LinkName) suite.Assert().EqualValues(256, r.TypedSpec().DHCP4.RouteMetric) - case "dhcp4/eth4.25": + case "configuration/dhcp4/eth4.25": suite.Assert().Equal("eth4.25", r.TypedSpec().LinkName) suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.TypedSpec().DHCP4.RouteMetric) } @@ -306,9 +310,11 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP4() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertNoOperators([]string{ - "dhcp4/eth0", - "dhcp4/eth2", - "dhcp4/eth4.26", + "configuration/dhcp4/eth0", + "default/dhcp4/eth0", + "configuration/dhcp4/eth2", + "default/dhcp4/eth2", + "configuration/dhcp4/eth4.26", }) })) } @@ -365,17 +371,17 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP6() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertOperators([]string{ - "dhcp6/eth2", - "dhcp6/eth3", + "configuration/dhcp6/eth2", + "configuration/dhcp6/eth3", }, func(r *network.OperatorSpec) error { suite.Assert().Equal(network.OperatorDHCP6, r.TypedSpec().Operator) suite.Assert().True(r.TypedSpec().RequireUp) switch r.Metadata().ID() { - case "dhcp6/eth2": + case "configuration/dhcp6/eth2": suite.Assert().Equal("eth2", r.TypedSpec().LinkName) suite.Assert().EqualValues(netctrl.DefaultRouteMetric, r.TypedSpec().DHCP6.RouteMetric) - case "dhcp6/eth3": + case "configuration/dhcp6/eth3": suite.Assert().Equal("eth3", r.TypedSpec().LinkName) suite.Assert().EqualValues(512, r.TypedSpec().DHCP6.RouteMetric) } @@ -387,7 +393,7 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationDHCP6() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertNoOperators([]string{ - "dhcp6/eth1", + "configuration/dhcp6/eth1", }) })) } @@ -448,21 +454,21 @@ func (suite *OperatorConfigSuite) TestMachineConfigurationVIP() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { return suite.assertOperators([]string{ - "vip/eth1", - "vip/eth2", - "vip/eth3.26", + "configuration/vip/eth1", + "configuration/vip/eth2", + "configuration/vip/eth3.26", }, func(r *network.OperatorSpec) error { suite.Assert().Equal(network.OperatorVIP, r.TypedSpec().Operator) suite.Assert().True(r.TypedSpec().RequireUp) switch r.Metadata().ID() { - case "vip/eth1": + case "configuration/vip/eth1": suite.Assert().Equal("eth1", r.TypedSpec().LinkName) suite.Assert().EqualValues(netaddr.MustParseIP("2.3.4.5"), r.TypedSpec().VIP.IP) - case "vip/eth2": + case "configuration/vip/eth2": suite.Assert().Equal("eth2", r.TypedSpec().LinkName) suite.Assert().EqualValues(netaddr.MustParseIP("fd7a:115c:a1e0:ab12:4843:cd96:6277:2302"), r.TypedSpec().VIP.IP) - case "vip/eth3.26": + case "configuration/vip/eth3.26": suite.Assert().Equal("eth3.26", r.TypedSpec().LinkName) suite.Assert().EqualValues(netaddr.MustParseIP("5.5.4.4"), r.TypedSpec().VIP.IP) } @@ -490,7 +496,7 @@ func (suite *OperatorConfigSuite) TearDownTest() { suite.Require().NoError(err) - suite.Assert().NoError(suite.state.Create(context.Background(), network.NewLinkStatus(network.NamespaceName, "bar"))) + suite.Assert().NoError(suite.state.Create(context.Background(), network.NewLinkStatus(network.ConfigNamespaceName, "bar"))) } func TestOperatorConfigSuite(t *testing.T) { diff --git a/internal/app/machined/pkg/controllers/network/operator_merge.go b/internal/app/machined/pkg/controllers/network/operator_merge.go new file mode 100644 index 000000000..f66e93ce1 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/operator_merge.go @@ -0,0 +1,140 @@ +// 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 provides controllers which manage network resources. +// +//nolint:dupl +package network + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/talos-systems/talos/pkg/machinery/resources/network" +) + +// OperatorMergeController merges network.OperatorSpec in network.ConfigNamespace and produces final network.OperatorSpec in network.Namespace. +type OperatorMergeController struct{} + +// Name implements controller.Controller interface. +func (ctrl *OperatorMergeController) Name() string { + return "network.OperatorMergeController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *OperatorMergeController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: network.ConfigNamespaceName, + Type: network.OperatorSpecType, + Kind: controller.InputWeak, + }, + { + Namespace: network.NamespaceName, + Type: network.OperatorSpecType, + Kind: controller.InputDestroyReady, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *OperatorMergeController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: network.OperatorSpecType, + Kind: controller.OutputShared, + }, + } +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *OperatorMergeController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + } + + // list source network configuration resources + list, err := r.List(ctx, resource.NewMetadata(network.ConfigNamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing source network operators: %w", err) + } + + // operator is allowed as long as it's not duplicate, for duplicate higher layer takes precedence + operators := map[string]*network.OperatorSpec{} + + for _, res := range list.Items { + operator := res.(*network.OperatorSpec) //nolint:errcheck,forcetypeassert + id := network.OperatorID(operator.TypedSpec().Operator, operator.TypedSpec().LinkName) + + existing, ok := operators[id] + if ok && existing.TypedSpec().ConfigLayer > operator.TypedSpec().ConfigLayer { + // skip this operator, as existing one is higher layer + continue + } + + operators[id] = operator + } + + conflictsDetected := 0 + + for id, operator := range operators { + operator := operator + + if err = r.Modify(ctx, network.NewOperatorSpec(network.NamespaceName, id), func(res resource.Resource) error { + op := res.(*network.OperatorSpec) //nolint:errcheck,forcetypeassert + + *op.TypedSpec() = *operator.TypedSpec() + + return nil + }); err != nil { + if state.IsPhaseConflictError(err) { + // phase conflict, resource is being torn down, skip updating it and trigger reconcile + // later by failing the loop after all processing is done + conflictsDetected++ + + delete(operators, id) + } else { + return fmt.Errorf("error updating resource: %w", err) + } + } + } + + // list operators for cleanup + list, err = r.List(ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if _, ok := operators[res.Metadata().ID()]; !ok { + var okToDestroy bool + + okToDestroy, err = r.Teardown(ctx, res.Metadata()) + if err != nil { + return fmt.Errorf("error cleaning up operators: %w", err) + } + + if okToDestroy { + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error cleaning up operators: %w", err) + } + } + } + } + + if conflictsDetected > 0 { + return fmt.Errorf("%d conflict(s) detected", conflictsDetected) + } + } +} diff --git a/internal/app/machined/pkg/controllers/network/operator_merge_test.go b/internal/app/machined/pkg/controllers/network/operator_merge_test.go new file mode 100644 index 000000000..4a2c2b1c1 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/operator_merge_test.go @@ -0,0 +1,276 @@ +// 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/. + +//nolint:dupl +package network_test + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "time" + + "github.com/cosi-project/runtime/pkg/controller/runtime" + "github.com/cosi-project/runtime/pkg/resource" + "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/stretchr/testify/suite" + "github.com/talos-systems/go-retry/retry" + "golang.org/x/sync/errgroup" + + netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/resources/network" +) + +type OperatorMergeSuite struct { + suite.Suite + + state state.State + + runtime *runtime.Runtime + wg sync.WaitGroup + + ctx context.Context + ctxCancel context.CancelFunc +} + +func (suite *OperatorMergeSuite) 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, logging.Wrap(log.Writer())) + suite.Require().NoError(err) + + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.OperatorMergeController{})) + + suite.startRuntime() +} + +func (suite *OperatorMergeSuite) startRuntime() { + suite.wg.Add(1) + + go func() { + defer suite.wg.Done() + + suite.Assert().NoError(suite.runtime.Run(suite.ctx)) + }() +} + +func (suite *OperatorMergeSuite) assertOperators(requiredIDs []string, check func(*network.OperatorSpec) error) error { + missingIDs := make(map[string]struct{}, len(requiredIDs)) + + for _, id := range requiredIDs { + missingIDs[id] = struct{}{} + } + + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + _, required := missingIDs[res.Metadata().ID()] + if !required { + continue + } + + delete(missingIDs, res.Metadata().ID()) + + if err = check(res.(*network.OperatorSpec)); err != nil { + return retry.ExpectedError(err) + } + } + + if len(missingIDs) > 0 { + return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) + } + + return nil +} + +func (suite *OperatorMergeSuite) assertNoOperator(id string) error { + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "", resource.VersionUndefined)) + if err != nil { + return err + } + + for _, res := range resources.Items { + if res.Metadata().ID() == id { + return retry.ExpectedError(fmt.Errorf("operator %q is still there", id)) + } + } + + return nil +} + +func (suite *OperatorMergeSuite) TestMerge() { + dhcp1 := network.NewOperatorSpec(network.ConfigNamespaceName, "default/dhcp4/eth0") + *dhcp1.TypedSpec() = network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: "eth0", + ConfigLayer: network.ConfigDefault, + } + + dhcp2 := network.NewOperatorSpec(network.ConfigNamespaceName, "configuration/dhcp4/eth0") + *dhcp2.TypedSpec() = network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: "eth0", + RequireUp: true, + ConfigLayer: network.ConfigMachineConfiguration, + } + + dhcp6 := network.NewOperatorSpec(network.ConfigNamespaceName, "configuration/dhcp6/eth0") + *dhcp6.TypedSpec() = network.OperatorSpecSpec{ + Operator: network.OperatorDHCP6, + LinkName: "eth0", + RequireUp: true, + ConfigLayer: network.ConfigMachineConfiguration, + } + + for _, res := range []resource.Resource{dhcp1, dhcp2, dhcp6} { + suite.Require().NoError(suite.state.Create(suite.ctx, res), "%v", res.Spec()) + } + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertOperators([]string{ + "dhcp4/eth0", + "dhcp6/eth0", + }, func(r *network.OperatorSpec) error { + switch r.Metadata().ID() { + case "dhcp4/eth0": + suite.Assert().Equal(*dhcp2.TypedSpec(), *r.TypedSpec()) + case "dhcp6/eth0": + suite.Assert().Equal(*dhcp6.TypedSpec(), *r.TypedSpec()) + } + + return nil + }) + })) + + suite.Require().NoError(suite.state.Destroy(suite.ctx, dhcp6.Metadata())) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertOperators([]string{ + "dhcp4/eth0", + }, func(r *network.OperatorSpec) error { + return nil + }) + })) + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertNoOperator("dhcp6/eth0") + })) +} + +//nolint:gocyclo +func (suite *OperatorMergeSuite) TestMergeFlapping() { + // simulate two conflicting operator definitions which are getting removed/added constantly + dhcp := network.NewOperatorSpec(network.ConfigNamespaceName, "default/dhcp4/eth0") + *dhcp.TypedSpec() = network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: "eth0", + ConfigLayer: network.ConfigDefault, + } + + override := network.NewOperatorSpec(network.ConfigNamespaceName, "configuration/dhcp4/eth0") + *override.TypedSpec() = network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: "eth0", + RequireUp: true, + ConfigLayer: network.ConfigMachineConfiguration, + } + + resources := []resource.Resource{dhcp, override} + + flipflop := func(idx int) func() error { + return func() error { + for i := 0; i < 500; i++ { + if err := suite.state.Create(suite.ctx, resources[idx]); err != nil { + return err + } + + if err := suite.state.Destroy(suite.ctx, resources[idx].Metadata()); err != nil { + return err + } + + time.Sleep(time.Millisecond) + } + + return suite.state.Create(suite.ctx, resources[idx]) + } + } + + var eg errgroup.Group + + eg.Go(flipflop(0)) + eg.Go(flipflop(1)) + eg.Go(func() error { + // add/remove finalizer to the merged resource + for i := 0; i < 1000; i++ { + if err := suite.state.AddFinalizer(suite.ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "dhcp4/eth0", resource.VersionUndefined), "foo"); err != nil { + if !state.IsNotFoundError(err) { + return err + } + + continue + } else { + suite.T().Log("finalizer added") + } + + time.Sleep(10 * time.Millisecond) + + if err := suite.state.RemoveFinalizer(suite.ctx, resource.NewMetadata(network.NamespaceName, network.OperatorSpecType, "dhcp4/eth0", resource.VersionUndefined), "foo"); err != nil { + if err != nil && !state.IsNotFoundError(err) { + return err + } + } + } + + return nil + }) + + suite.Require().NoError(eg.Wait()) + + suite.Assert().NoError(retry.Constant(15*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertOperators([]string{ + "dhcp4/eth0", + }, func(r *network.OperatorSpec) error { + if r.Metadata().Phase() != resource.PhaseRunning { + return retry.ExpectedErrorf("resource phase is %s", r.Metadata().Phase()) + } + + if *override.TypedSpec() != *r.TypedSpec() { + // using retry here, as it might not be reconciled immediately + return retry.ExpectedError(fmt.Errorf("not equal yet")) + } + + return nil + }) + })) +} + +func (suite *OperatorMergeSuite) TearDownTest() { + suite.T().Log("tear down") + + suite.ctxCancel() + + suite.wg.Wait() + + // trigger updates in resources to stop watch loops + suite.Assert().NoError(suite.state.Create(context.Background(), network.NewOperatorSpec(network.ConfigNamespaceName, "bar"))) +} + +func TestOperatorMergeSuite(t *testing.T) { + suite.Run(t, new(OperatorMergeSuite)) +} diff --git a/internal/app/machined/pkg/controllers/network/platform_config.go b/internal/app/machined/pkg/controllers/network/platform_config.go index 024eb5473..643120d08 100644 --- a/internal/app/machined/pkg/controllers/network/platform_config.go +++ b/internal/app/machined/pkg/controllers/network/platform_config.go @@ -5,19 +5,29 @@ package network import ( + "bytes" "context" - "errors" "fmt" + "os" + "path/filepath" + "sync" + "time" + "github.com/AlekSi/pointer" + "github.com/cenkalti/backoff/v4" "github.com/cosi-project/runtime/pkg/controller" "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/state" "go.uber.org/zap" + "gopkg.in/yaml.v3" "inet.af/netaddr" v1alpha1runtime "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" - platformerrors "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/machinery/constants" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" + "github.com/talos-systems/talos/pkg/machinery/resources/v1alpha1" ) // Virtual link name for external IPs. @@ -26,6 +36,7 @@ const externalLink = "external" // PlatformConfigController manages updates hostnames and addressstatuses based on platform information. type PlatformConfigController struct { V1alpha1Platform v1alpha1runtime.Platform + StatePath string } // Name implements controller.Controller interface. @@ -35,27 +46,62 @@ func (ctrl *PlatformConfigController) Name() string { // Inputs implements controller.Controller interface. func (ctrl *PlatformConfigController) Inputs() []controller.Input { - return nil + return []controller.Input{ + { + Namespace: v1alpha1.NamespaceName, + Type: runtimeres.MountStatusType, + ID: pointer.ToString(constants.StatePartitionLabel), + Kind: controller.InputWeak, + }, + } } // Outputs implements controller.Controller interface. func (ctrl *PlatformConfigController) Outputs() []controller.Output { return []controller.Output{ + { + Type: network.AddressSpecType, + Kind: controller.OutputShared, + }, + { + Type: network.LinkSpecType, + Kind: controller.OutputShared, + }, + { + Type: network.RouteSpecType, + Kind: controller.OutputShared, + }, { Type: network.HostnameSpecType, Kind: controller.OutputShared, }, + { + Type: network.ResolverSpecType, + Kind: controller.OutputShared, + }, + { + Type: network.TimeServerSpecType, + Kind: controller.OutputShared, + }, { Type: network.AddressStatusType, Kind: controller.OutputShared, }, + { + Type: network.OperatorSpecType, + Kind: controller.OutputShared, + }, } } // Run implements controller.Controller interface. // -//nolint:gocyclo +//nolint:gocyclo,cyclop func (ctrl *PlatformConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + if ctrl.StatePath == "" { + ctrl.StatePath = constants.StateMountPoint + } + select { case <-ctx.Done(): return nil @@ -67,87 +113,421 @@ func (ctrl *PlatformConfigController) Run(ctx context.Context, r controller.Runt return nil } - // platform is fetched only once (but controller might fail and restart if fetching platform fails) - hostname, err := ctrl.V1alpha1Platform.Hostname(ctx) - if err != nil { - if !errors.Is(err, platformerrors.ErrNoHostname) { - return fmt.Errorf("error getting hostname: %w", err) + platformCtx, platformCtxCancel := context.WithCancel(ctx) + defer platformCtxCancel() + + platformCh := make(chan *v1alpha1runtime.PlatformNetworkConfig, 1) + + var platformWg sync.WaitGroup + + platformWg.Add(1) + + go func() { + defer platformWg.Done() + + ctrl.runWithRestarts(platformCtx, logger, func() error { + return ctrl.V1alpha1Platform.NetworkConfiguration(platformCtx, platformCh) + }) + }() + + defer platformWg.Wait() + + r.QueueReconcile() + + var cachedNetworkConfig, networkConfig *v1alpha1runtime.PlatformNetworkConfig + + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + case networkConfig = <-platformCh: } - } - if len(hostname) > 0 { - id := network.LayeredID(network.ConfigPlatform, network.HostnameID) + var stateMounted bool - if err = r.Modify( - ctx, - network.NewHostnameSpec(network.ConfigNamespaceName, id), - func(r resource.Resource) error { - r.(*network.HostnameSpec).TypedSpec().ConfigLayer = network.ConfigPlatform - - return r.(*network.HostnameSpec).TypedSpec().ParseFQDN(string(hostname)) - }, - ); err != nil { - return fmt.Errorf("error modifying hostname resource: %w", err) - } - } - - externalIPs, err := ctrl.V1alpha1Platform.ExternalIPs(ctx) - if err != nil { - if !errors.Is(err, platformerrors.ErrNoExternalIPs) { - return fmt.Errorf("error getting external IPs: %w", err) - } - } - - touchedIDs := make(map[resource.ID]struct{}) - - for _, addr := range externalIPs { - addr := addr - - ipAddr, _ := netaddr.FromStdIP(addr) - ipPrefix := netaddr.IPPrefixFrom(ipAddr, ipAddr.BitLen()) - id := network.AddressID(externalLink, ipPrefix) - - if err = r.Modify(ctx, network.NewAddressStatus(network.NamespaceName, id), func(r resource.Resource) error { - status := r.(*network.AddressStatus).TypedSpec() - - status.Address = ipPrefix - status.LinkName = externalLink - - if ipAddr.Is4() { - status.Family = nethelpers.FamilyInet4 + if _, err := r.Get(ctx, resource.NewMetadata(v1alpha1.NamespaceName, runtimeres.MountStatusType, constants.StatePartitionLabel, resource.VersionUndefined)); err == nil { + stateMounted = true + } else { + if state.IsNotFoundError(err) { + // in container mode STATE is always mounted + if ctrl.V1alpha1Platform.Mode() == v1alpha1runtime.ModeContainer { + stateMounted = true + } } else { - status.Family = nethelpers.FamilyInet6 + return fmt.Errorf("error reading mount status: %w", err) + } + } + + if stateMounted && cachedNetworkConfig == nil { + var err error + + cachedNetworkConfig, err = ctrl.loadConfig(filepath.Join(ctrl.StatePath, constants.PlatformNetworkConfigFilename)) + if err != nil { + logger.Warn("ignored failure loading cached platform network config", zap.Error(err)) + } else if cachedNetworkConfig != nil { + logger.Debug("loaded cached platform network config") + } + } + + if stateMounted && networkConfig != nil { + if err := ctrl.storeConfig(filepath.Join(ctrl.StatePath, constants.PlatformNetworkConfigFilename), networkConfig); err != nil { + return fmt.Errorf("error saving config: %w", err) } - status.Scope = nethelpers.ScopeGlobal + logger.Debug("stored cached platform network config") - return nil - }); err != nil { - return fmt.Errorf("error modifying resource: %w", err) + cachedNetworkConfig = networkConfig } - touchedIDs[id] = struct{}{} + switch { + // prefer live network config over cached config always + case networkConfig != nil: + if err := ctrl.apply(ctx, r, networkConfig); err != nil { + return err + } + // cached network is only used as last resort + case cachedNetworkConfig != nil: + if err := ctrl.apply(ctx, r, cachedNetworkConfig); err != nil { + return err + } + } } +} - // list resources for cleanup - list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.AddressStatusType, "", resource.VersionUndefined)) - if err != nil { - return fmt.Errorf("error listing resources: %w", err) - } +//nolint:dupl,gocyclo +func (ctrl *PlatformConfigController) apply(ctx context.Context, r controller.Runtime, networkConfig *v1alpha1runtime.PlatformNetworkConfig) error { + // handle all network specs in a loop as all specs can be handled in a similar way + for _, specType := range []struct { + length int + getter func(i int) interface{} + idBuilder func(spec interface{}) resource.ID + resourceBuilder func(id string) resource.Resource + resourceModifier func(newSpec interface{}) func(r resource.Resource) error + }{ + // AddressSpec + { + length: len(networkConfig.Addresses), + getter: func(i int) interface{} { + return networkConfig.Addresses[i] + }, + idBuilder: func(spec interface{}) resource.ID { + addressSpec := spec.(network.AddressSpecSpec) //nolint:errcheck,forcetypeassert - for _, res := range list.Items { - if res.Metadata().Owner() != ctrl.Name() { + return network.LayeredID(network.ConfigPlatform, network.AddressID(addressSpec.LinkName, addressSpec.Address)) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewAddressSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.AddressSpec).TypedSpec() + + *spec = newSpec.(network.AddressSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // LinkSpec + { + length: len(networkConfig.Links), + getter: func(i int) interface{} { + return networkConfig.Links[i] + }, + idBuilder: func(spec interface{}) resource.ID { + linkSpec := spec.(network.LinkSpecSpec) //nolint:errcheck,forcetypeassert + + return network.LayeredID(network.ConfigPlatform, network.LinkID(linkSpec.Name)) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewLinkSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.LinkSpec).TypedSpec() + + *spec = newSpec.(network.LinkSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // RouteSpec + { + length: len(networkConfig.Routes), + getter: func(i int) interface{} { + return networkConfig.Routes[i] + }, + idBuilder: func(spec interface{}) resource.ID { + routeSpec := spec.(network.RouteSpecSpec) //nolint:errcheck,forcetypeassert + + return network.LayeredID(network.ConfigPlatform, network.RouteID(routeSpec.Table, routeSpec.Family, routeSpec.Destination, routeSpec.Gateway, routeSpec.Priority)) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewRouteSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.RouteSpec).TypedSpec() + + *spec = newSpec.(network.RouteSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // HostnameSpec + { + length: len(networkConfig.Hostnames), + getter: func(i int) interface{} { + return networkConfig.Hostnames[i] + }, + idBuilder: func(spec interface{}) resource.ID { + return network.LayeredID(network.ConfigPlatform, network.HostnameID) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewHostnameSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.HostnameSpec).TypedSpec() + + *spec = newSpec.(network.HostnameSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // ResolverSpec + { + length: len(networkConfig.Resolvers), + getter: func(i int) interface{} { + return networkConfig.Resolvers[i] + }, + idBuilder: func(spec interface{}) resource.ID { + return network.LayeredID(network.ConfigPlatform, network.ResolverID) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewResolverSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.ResolverSpec).TypedSpec() + + *spec = newSpec.(network.ResolverSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // TimeServerSpec + { + length: len(networkConfig.TimeServers), + getter: func(i int) interface{} { + return networkConfig.TimeServers[i] + }, + idBuilder: func(spec interface{}) resource.ID { + return network.LayeredID(network.ConfigPlatform, network.TimeServerID) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewTimeServerSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.TimeServerSpec).TypedSpec() + + *spec = newSpec.(network.TimeServerSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // OperatorSpec + { + length: len(networkConfig.Operators), + getter: func(i int) interface{} { + return networkConfig.Operators[i] + }, + idBuilder: func(spec interface{}) resource.ID { + operatorSpec := spec.(network.OperatorSpecSpec) //nolint:errcheck,forcetypeassert + + return network.LayeredID(network.ConfigPlatform, network.OperatorID(operatorSpec.Operator, operatorSpec.LinkName)) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewOperatorSpec(network.ConfigNamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + spec := r.(*network.OperatorSpec).TypedSpec() + + *spec = newSpec.(network.OperatorSpecSpec) //nolint:errcheck,forcetypeassert + spec.ConfigLayer = network.ConfigPlatform + + return nil + } + }, + }, + // ExternalIPs + { + length: len(networkConfig.ExternalIPs), + getter: func(i int) interface{} { + return networkConfig.ExternalIPs[i] + }, + idBuilder: func(spec interface{}) resource.ID { + ipAddr := spec.(netaddr.IP) //nolint:errcheck,forcetypeassert + ipPrefix := netaddr.IPPrefixFrom(ipAddr, ipAddr.BitLen()) + + return network.AddressID(externalLink, ipPrefix) + }, + resourceBuilder: func(id string) resource.Resource { + return network.NewAddressStatus(network.NamespaceName, id) + }, + resourceModifier: func(newSpec interface{}) func(r resource.Resource) error { + return func(r resource.Resource) error { + ipAddr := newSpec.(netaddr.IP) //nolint:errcheck,forcetypeassert + ipPrefix := netaddr.IPPrefixFrom(ipAddr, ipAddr.BitLen()) + + status := r.(*network.AddressStatus).TypedSpec() + + status.Address = ipPrefix + status.LinkName = externalLink + + if ipAddr.Is4() { + status.Family = nethelpers.FamilyInet4 + } else { + status.Family = nethelpers.FamilyInet6 + } + + status.Scope = nethelpers.ScopeGlobal + + return nil + } + }, + }, + } { + if specType.length == 0 { continue } - if _, ok := touchedIDs[res.Metadata().ID()]; ok { - continue + touchedIDs := make(map[resource.ID]struct{}, specType.length) + + resourceEmpty := specType.resourceBuilder("") + resourceNamespace := resourceEmpty.Metadata().Namespace() + resourceType := resourceEmpty.Metadata().Type() + + for i := 0; i < specType.length; i++ { + spec := specType.getter(i) + id := specType.idBuilder(spec) + + if err := r.Modify(ctx, specType.resourceBuilder(id), specType.resourceModifier(spec)); err != nil { + return fmt.Errorf("error modifying resource %s: %w", resourceType, err) + } + + touchedIDs[id] = struct{}{} } - if err = r.Destroy(ctx, res.Metadata()); err != nil { - return fmt.Errorf("error deleting address status %s: %w", res, err) + list, err := r.List(ctx, resource.NewMetadata(resourceNamespace, resourceType, "", resource.VersionUndefined)) + if err != nil { + return fmt.Errorf("error listing resources: %w", err) + } + + for _, res := range list.Items { + if res.Metadata().Owner() != ctrl.Name() { + continue + } + + if _, ok := touchedIDs[res.Metadata().ID()]; ok { + continue + } + + if err = r.Destroy(ctx, res.Metadata()); err != nil { + return fmt.Errorf("error deleting %s: %w", res, err) + } } } return nil } + +func (ctrl *PlatformConfigController) runWithRestarts(ctx context.Context, logger *zap.Logger, f func() error) { + backoff := backoff.NewExponentialBackOff() + + // disable number of retries limit + backoff.MaxElapsedTime = 0 + + for ctx.Err() == nil { + var err error + + if err = ctrl.runWithPanicHandler(logger, f); err == nil { + // operator finished without an error + return + } + + interval := backoff.NextBackOff() + + logger.Error("restarting platform network config", zap.Duration("interval", interval), zap.Error(err)) + + select { + case <-ctx.Done(): + return + case <-time.After(interval): + } + } +} + +func (ctrl *PlatformConfigController) runWithPanicHandler(logger *zap.Logger, f func() error) (err error) { + defer func() { + if p := recover(); p != nil { + err = fmt.Errorf("panic: %v", p) + + logger.Error("platform panicked", zap.Stack("stack"), zap.Error(err)) + } + }() + + err = f() + + return +} + +func (ctrl *PlatformConfigController) loadConfig(path string) (*v1alpha1runtime.PlatformNetworkConfig, error) { + marshaled, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, err + } + + var networkConfig v1alpha1runtime.PlatformNetworkConfig + + if err = yaml.Unmarshal(marshaled, &networkConfig); err != nil { + return nil, fmt.Errorf("error unmarshaling network config: %w", err) + } + + return &networkConfig, nil +} + +func (ctrl *PlatformConfigController) storeConfig(path string, networkConfig *v1alpha1runtime.PlatformNetworkConfig) error { + marshaled, err := yaml.Marshal(networkConfig) + if err != nil { + return fmt.Errorf("error marshaling network config: %w", err) + } + + if _, err := os.Stat(path); err == nil { + existing, err := os.ReadFile(path) + if err == nil && bytes.Equal(marshaled, existing) { + // existing contents are identical, skip writing to avoid no-op writes + return nil + } + } + + return os.WriteFile(path, marshaled, 0o400) +} diff --git a/internal/app/machined/pkg/controllers/network/platform_config_test.go b/internal/app/machined/pkg/controllers/network/platform_config_test.go index a95bcf29b..ed0445a05 100644 --- a/internal/app/machined/pkg/controllers/network/platform_config_test.go +++ b/internal/app/machined/pkg/controllers/network/platform_config_test.go @@ -9,7 +9,8 @@ import ( "context" "fmt" "log" - "net" + "os" + "path/filepath" "sync" "testing" "time" @@ -22,12 +23,16 @@ import ( "github.com/stretchr/testify/suite" "github.com/talos-systems/go-procfs/procfs" "github.com/talos-systems/go-retry/retry" + "inet.af/netaddr" netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" v1alpha1runtime "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/pkg/logging" + "github.com/talos-systems/talos/pkg/machinery/constants" "github.com/talos-systems/talos/pkg/machinery/nethelpers" "github.com/talos-systems/talos/pkg/machinery/resources/network" + runtimeres "github.com/talos-systems/talos/pkg/machinery/resources/runtime" + "github.com/talos-systems/talos/pkg/machinery/resources/v1alpha1" ) type PlatformConfigSuite struct { @@ -35,6 +40,8 @@ type PlatformConfigSuite struct { state state.State + statePath string + runtime *runtime.Runtime wg sync.WaitGroup @@ -51,6 +58,8 @@ func (suite *PlatformConfigSuite) SetupTest() { suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer())) suite.Require().NoError(err) + + suite.statePath = suite.T().TempDir() } func (suite *PlatformConfigSuite) startRuntime() { @@ -63,29 +72,26 @@ func (suite *PlatformConfigSuite) startRuntime() { }() } -func (suite *PlatformConfigSuite) assertHostnames(requiredIDs []string, check func(*network.HostnameSpec) error) error { +func (suite *PlatformConfigSuite) assertResources(resourceNamespace resource.Namespace, resourceType resource.Type, requiredIDs []string, check func(resource.Resource) error) error { missingIDs := make(map[string]struct{}, len(requiredIDs)) for _, id := range requiredIDs { missingIDs[id] = struct{}{} } - resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(resourceNamespace, resourceType, "", resource.VersionUndefined)) if err != nil { return err } for _, res := range resources.Items { - _, required := missingIDs[res.Metadata().ID()] - if !required { - continue + if _, ok := missingIDs[res.Metadata().ID()]; ok { + if err = check(res); err != nil { + return retry.ExpectedError(err) + } } delete(missingIDs, res.Metadata().ID()) - - if err = check(res.(*network.HostnameSpec)); err != nil { - return retry.ExpectedError(err) - } } if len(missingIDs) > 0 { @@ -95,8 +101,8 @@ func (suite *PlatformConfigSuite) assertHostnames(requiredIDs []string, check fu return nil } -func (suite *PlatformConfigSuite) assertNoHostname(id string) error { - resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, network.HostnameSpecType, "", resource.VersionUndefined)) +func (suite *PlatformConfigSuite) assertNoResource(resourceType resource.Type, id string) error { + resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.ConfigNamespaceName, resourceType, "", resource.VersionUndefined)) if err != nil { return err } @@ -110,38 +116,6 @@ func (suite *PlatformConfigSuite) assertNoHostname(id string) error { return nil } -func (suite *PlatformConfigSuite) assertAddresses(requiredIDs []string, check func(*network.AddressStatus) error) error { - missingIDs := make(map[string]struct{}, len(requiredIDs)) - - for _, id := range requiredIDs { - missingIDs[id] = struct{}{} - } - - resources, err := suite.state.List(suite.ctx, resource.NewMetadata(network.NamespaceName, network.AddressStatusType, "", resource.VersionUndefined)) - if err != nil { - return err - } - - for _, res := range resources.Items { - _, required := missingIDs[res.Metadata().ID()] - if !required { - continue - } - - delete(missingIDs, res.Metadata().ID()) - - if err = check(res.(*network.AddressStatus)); err != nil { - return retry.ExpectedError(err) - } - } - - if len(missingIDs) > 0 { - return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs)) - } - - return nil -} - func (suite *PlatformConfigSuite) TestNoPlatform() { suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{})) @@ -149,46 +123,213 @@ func (suite *PlatformConfigSuite) TestNoPlatform() { suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { - return suite.assertNoHostname("platform/hostname") + return suite.assertNoResource(network.HostnameSpecType, "platform/hostname") })) } -func (suite *PlatformConfigSuite) TestPlatformMock() { +func (suite *PlatformConfigSuite) TestPlatformMockHostname() { suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ V1alpha1Platform: &platformMock{hostname: []byte("talos-e2e-897b4e49-gcp-controlplane-jvcnl.c.talos-testbed.internal")}, + StatePath: suite.statePath, })) suite.startRuntime() suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { - return suite.assertHostnames([]string{ + return suite.assertResources(network.ConfigNamespaceName, network.HostnameSpecType, []string{ "platform/hostname", - }, func(r *network.HostnameSpec) error { - suite.Assert().Equal("talos-e2e-897b4e49-gcp-controlplane-jvcnl", r.TypedSpec().Hostname) - suite.Assert().Equal("c.talos-testbed.internal", r.TypedSpec().Domainname) - suite.Assert().Equal(network.ConfigPlatform, r.TypedSpec().ConfigLayer) + }, func(r resource.Resource) error { + spec := r.(*network.HostnameSpec).TypedSpec() + + suite.Assert().Equal("talos-e2e-897b4e49-gcp-controlplane-jvcnl", spec.Hostname) + suite.Assert().Equal("c.talos-testbed.internal", spec.Domainname) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) return nil }) })) } -func (suite *PlatformConfigSuite) TestPlatformMockNoDomain() { +func (suite *PlatformConfigSuite) TestPlatformMockHostnameNoDomain() { suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ V1alpha1Platform: &platformMock{hostname: []byte("talos-e2e-897b4e49-gcp-controlplane-jvcnl")}, + StatePath: suite.statePath, })) suite.startRuntime() suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { - return suite.assertHostnames([]string{ + return suite.assertResources(network.ConfigNamespaceName, network.HostnameSpecType, []string{ "platform/hostname", - }, func(r *network.HostnameSpec) error { - suite.Assert().Equal("talos-e2e-897b4e49-gcp-controlplane-jvcnl", r.TypedSpec().Hostname) - suite.Assert().Equal("", r.TypedSpec().Domainname) - suite.Assert().Equal(network.ConfigPlatform, r.TypedSpec().ConfigLayer) + }, func(r resource.Resource) error { + spec := r.(*network.HostnameSpec).TypedSpec() + + suite.Assert().Equal("talos-e2e-897b4e49-gcp-controlplane-jvcnl", spec.Hostname) + suite.Assert().Equal("", spec.Domainname) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + +func (suite *PlatformConfigSuite) TestPlatformMockAddresses() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + addresses: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("192.168.1.24/24"), netaddr.MustParseIPPrefix("2001:fd::3/64")}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.AddressSpecType, []string{ + "platform/eth0/192.168.1.24/24", + "platform/eth0/2001:fd::3/64", + }, func(r resource.Resource) error { + spec := r.(*network.AddressSpec).TypedSpec() + + switch r.Metadata().ID() { + case "platform/eth0/192.168.1.24/24": + suite.Assert().Equal(nethelpers.FamilyInet4, spec.Family) + suite.Assert().Equal("192.168.1.24/24", spec.Address.String()) + case "platform/eth0/2001:fd::3/64": + suite.Assert().Equal(nethelpers.FamilyInet6, spec.Family) + suite.Assert().Equal("2001:fd::3/64", spec.Address.String()) + } + + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + +func (suite *PlatformConfigSuite) TestPlatformMockLinks() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + linksUp: []string{"eth0", "eth1"}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.LinkSpecType, []string{ + "platform/eth0", + "platform/eth1", + }, func(r resource.Resource) error { + spec := r.(*network.LinkSpec).TypedSpec() + + suite.Assert().True(spec.Up) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + +func (suite *PlatformConfigSuite) TestPlatformMockRoutes() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + defaultRoutes: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.RouteSpecType, []string{ + "platform/inet4/10.0.0.1//1024", + }, func(r resource.Resource) error { + spec := r.(*network.RouteSpec).TypedSpec() + + suite.Assert().Equal("10.0.0.1", spec.Gateway.String()) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + +func (suite *PlatformConfigSuite) TestPlatformMockOperators() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + dhcp4Links: []string{"eth1", "eth2"}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.OperatorSpecType, []string{ + "platform/dhcp4/eth1", + "platform/dhcp4/eth2", + }, func(r resource.Resource) error { + spec := r.(*network.OperatorSpec).TypedSpec() + + suite.Assert().Equal(network.OperatorDHCP4, spec.Operator) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + +func (suite *PlatformConfigSuite) TestPlatformMockResolvers() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + resolvers: []netaddr.IP{netaddr.MustParseIP("1.1.1.1")}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.ResolverSpecType, []string{ + "platform/resolvers", + }, func(r resource.Resource) error { + spec := r.(*network.ResolverSpec).TypedSpec() + + suite.Assert().Equal("[1.1.1.1]", fmt.Sprintf("%s", spec.DNSServers)) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + +func (suite *PlatformConfigSuite) TestPlatformMockTimeServers() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + timeServers: []string{"pool.ntp.org"}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.TimeServerSpecType, []string{ + "platform/timeservers", + }, func(r resource.Resource) error { + spec := r.(*network.TimeServerSpec).TypedSpec() + + suite.Assert().Equal("[pool.ntp.org]", fmt.Sprintf("%s", spec.NTPServers)) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) return nil }) @@ -197,24 +338,27 @@ func (suite *PlatformConfigSuite) TestPlatformMockNoDomain() { func (suite *PlatformConfigSuite) TestPlatformMockExternalIPs() { suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ - V1alpha1Platform: &platformMock{ipAddresses: []net.IP{net.ParseIP("10.3.4.5"), net.ParseIP("2001:470:6d:30e:96f4:4219:5733:b860")}}, + V1alpha1Platform: &platformMock{externalIPs: []netaddr.IP{netaddr.MustParseIP("10.3.4.5"), netaddr.MustParseIP("2001:470:6d:30e:96f4:4219:5733:b860")}}, + StatePath: suite.statePath, })) suite.startRuntime() suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( func() error { - return suite.assertAddresses([]string{ + return suite.assertResources(network.NamespaceName, network.AddressStatusType, []string{ "external/10.3.4.5/32", "external/2001:470:6d:30e:96f4:4219:5733:b860/128", - }, func(r *network.AddressStatus) error { - suite.Assert().Equal("external", r.TypedSpec().LinkName) - suite.Assert().Equal(nethelpers.ScopeGlobal, r.TypedSpec().Scope) + }, func(r resource.Resource) error { + spec := r.(*network.AddressStatus).TypedSpec() + + suite.Assert().Equal("external", spec.LinkName) + suite.Assert().Equal(nethelpers.ScopeGlobal, spec.Scope) if r.Metadata().ID() == "external/10.3.4.5/32" { - suite.Assert().Equal(nethelpers.FamilyInet4, r.TypedSpec().Family) + suite.Assert().Equal(nethelpers.FamilyInet4, spec.Family) } else { - suite.Assert().Equal(nethelpers.FamilyInet6, r.TypedSpec().Family) + suite.Assert().Equal(nethelpers.FamilyInet6, spec.Family) } return nil @@ -222,6 +366,94 @@ func (suite *PlatformConfigSuite) TestPlatformMockExternalIPs() { })) } +const sampleStoredConfig = "addresses: []\nlinks: []\nroutes: []\nhostnames:\n - hostname: talos-e2e-897b4e49-gcp-controlplane-jvcnl\n domainname: \"\"\n layer: default\nresolvers: []\ntimeServers: []\noperators: []\nexternalIPs:\n - 10.3.4.5\n - 2001:470:6d:30e:96f4:4219:5733:b860\n" //nolint:lll + +func (suite *PlatformConfigSuite) TestStoreConfig() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + hostname: []byte("talos-e2e-897b4e49-gcp-controlplane-jvcnl"), + externalIPs: []netaddr.IP{netaddr.MustParseIP("10.3.4.5"), netaddr.MustParseIP("2001:470:6d:30e:96f4:4219:5733:b860")}, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + stateMount := runtimeres.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel) + + suite.Assert().NoError(suite.state.Create(suite.ctx, stateMount)) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + contents, err := os.ReadFile(filepath.Join(suite.statePath, constants.PlatformNetworkConfigFilename)) + if err != nil { + if os.IsNotExist(err) { + return retry.ExpectedError(err) + } + + return err + } + + suite.Assert().Equal(sampleStoredConfig, string(contents)) + + return nil + })) +} + +func (suite *PlatformConfigSuite) TestLoadConfig() { + suite.Require().NoError(suite.runtime.RegisterController(&netctrl.PlatformConfigController{ + V1alpha1Platform: &platformMock{ + noData: true, + }, + StatePath: suite.statePath, + })) + + suite.startRuntime() + + suite.Require().NoError(os.WriteFile(filepath.Join(suite.statePath, constants.PlatformNetworkConfigFilename), []byte(sampleStoredConfig), 0o400)) + + stateMount := runtimeres.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel) + + suite.Assert().NoError(suite.state.Create(suite.ctx, stateMount)) + + // controller should pick up cached network configuration + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.NamespaceName, network.AddressStatusType, []string{ + "external/10.3.4.5/32", + "external/2001:470:6d:30e:96f4:4219:5733:b860/128", + }, func(r resource.Resource) error { + spec := r.(*network.AddressStatus).TypedSpec() + + suite.Assert().Equal("external", spec.LinkName) + suite.Assert().Equal(nethelpers.ScopeGlobal, spec.Scope) + + if r.Metadata().ID() == "external/10.3.4.5/32" { + suite.Assert().Equal(nethelpers.FamilyInet4, spec.Family) + } else { + suite.Assert().Equal(nethelpers.FamilyInet6, spec.Family) + } + + return nil + }) + })) + + suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry( + func() error { + return suite.assertResources(network.ConfigNamespaceName, network.HostnameSpecType, []string{ + "platform/hostname", + }, func(r resource.Resource) error { + spec := r.(*network.HostnameSpec).TypedSpec() + + suite.Assert().Equal("talos-e2e-897b4e49-gcp-controlplane-jvcnl", spec.Hostname) + suite.Assert().Equal("", spec.Domainname) + suite.Assert().Equal(network.ConfigPlatform, spec.ConfigLayer) + + return nil + }) + })) +} + func (suite *PlatformConfigSuite) TearDownTest() { suite.T().Log("tear down") @@ -235,8 +467,16 @@ func TestPlatformConfigSuite(t *testing.T) { } type platformMock struct { - hostname []byte - ipAddresses []net.IP + noData bool + + hostname []byte + externalIPs []netaddr.IP + addresses []netaddr.IPPrefix + defaultRoutes []netaddr.IP + linksUp []string + resolvers []netaddr.IP + timeServers []string + dhcp4Links []string } func (mock *platformMock) Name() string { @@ -247,18 +487,108 @@ func (mock *platformMock) Configuration(context.Context) ([]byte, error) { return nil, nil } -func (mock *platformMock) Hostname(context.Context) ([]byte, error) { - return mock.hostname, nil -} - func (mock *platformMock) Mode() v1alpha1runtime.Mode { return v1alpha1runtime.ModeCloud } -func (mock *platformMock) ExternalIPs(context.Context) ([]net.IP, error) { - return mock.ipAddresses, nil -} - func (mock *platformMock) KernelArgs() procfs.Parameters { return nil } + +//nolint:gocyclo +func (mock *platformMock) NetworkConfiguration(ctx context.Context, ch chan<- *v1alpha1runtime.PlatformNetworkConfig) error { + if mock.noData { + return nil + } + + networkConfig := &v1alpha1runtime.PlatformNetworkConfig{ + ExternalIPs: mock.externalIPs, + } + + if mock.hostname != nil { + hostnameSpec := network.HostnameSpecSpec{} + if err := hostnameSpec.ParseFQDN(string(mock.hostname)); err != nil { + return err + } + + networkConfig.Hostnames = []network.HostnameSpecSpec{hostnameSpec} + } + + for _, addr := range mock.addresses { + family := nethelpers.FamilyInet4 + if addr.IP().Is6() { + family = nethelpers.FamilyInet6 + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: "eth0", + Address: addr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }) + } + + for _, gw := range mock.defaultRoutes { + family := nethelpers.FamilyInet4 + if gw.Is6() { + family = nethelpers.FamilyInet6 + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: "eth0", + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + + for _, link := range mock.linksUp { + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Name: link, + Up: true, + }) + } + + if len(mock.resolvers) > 0 { + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + ConfigLayer: network.ConfigPlatform, + DNSServers: mock.resolvers, + }) + } + + if len(mock.timeServers) > 0 { + networkConfig.TimeServers = append(networkConfig.TimeServers, network.TimeServerSpecSpec{ + ConfigLayer: network.ConfigPlatform, + NTPServers: mock.timeServers, + }) + } + + for _, link := range mock.dhcp4Links { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: link, + Operator: network.OperatorDHCP4, + DHCP4: network.DHCP4OperatorSpec{}, + }) + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/controller_test.go b/internal/app/machined/pkg/runtime/controller_test.go deleted file mode 100644 index 0e28c20c0..000000000 --- a/internal/app/machined/pkg/runtime/controller_test.go +++ /dev/null @@ -1,679 +0,0 @@ -// 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 runtime_test - -// import ( -// "fmt" -// "net" -// "testing" - -// "github.com/talos-systems/go-procfs/procfs" - -// "github.com/talos-systems/talos/pkg/machinery/api/machine" -// ) - -// type MockSuccessfulSequencer struct{} - -// // Boot is a mock method that overrides the embedded sequencer's Boot method. -// func (s *MockSuccessfulSequencer) Boot() []Phase { -// return []Phase{ -// &MockSuccessfulPhase{}, -// } -// } - -// // Initialize is a mock method that overrides the embedded sequencer's Initialize method. -// func (s *MockSuccessfulSequencer) Initialize() []Phase { -// return []Phase{ -// &MockSuccessfulPhase{}, -// } -// } - -// // Shutdown is a mock method that overrides the embedded sequencer's Shutdown method. -// func (s *MockSuccessfulSequencer) Shutdown() []Phase { -// return []Phase{ -// &MockSuccessfulPhase{}, -// } -// } - -// // Upgrade is a mock method that overrides the embedded sequencer's Upgrade method. -// func (s *MockSuccessfulSequencer) Upgrade(req *machine.UpgradeRequest) []Phase { -// return []Phase{ -// &MockSuccessfulPhase{}, -// } -// } - -// // Reboot is a mock method that overrides the embedded sequencer's Reboot method. -// func (s *MockSuccessfulSequencer) Reboot() []Phase { -// return []Phase{ -// &MockSuccessfulPhase{}, -// } -// } - -// // Reset is a mock method that overrides the embedded sequencer's Reset method. -// func (s *MockSuccessfulSequencer) Reset(req *machine.ResetRequest) []Phase { -// return []Phase{ -// &MockSuccessfulPhase{}, -// } -// } - -// type MockUnsuccessfulSequencer struct{} - -// // Boot is a mock method that overrides the embedded sequencer's Boot method. -// func (s *MockUnsuccessfulSequencer) Boot() []Phase { -// return []Phase{ -// &MockUnsuccessfulPhase{}, -// } -// } - -// // Initialize is a mock method that overrides the embedded sequencer's Initialize method. -// func (s *MockUnsuccessfulSequencer) Initialize() []Phase { -// return []Phase{ -// &MockUnsuccessfulPhase{}, -// } -// } - -// // Shutdown is a mock method that overrides the embedded sequencer's Shutdown method. -// func (s *MockUnsuccessfulSequencer) Shutdown() []Phase { -// return []Phase{ -// &MockUnsuccessfulPhase{}, -// } -// } - -// // Upgrade is a mock method that overrides the embedded sequencer's Upgrade method. -// func (s *MockUnsuccessfulSequencer) Upgrade(req *machine.UpgradeRequest) []Phase { -// return []Phase{ -// &MockUnsuccessfulPhase{}, -// } -// } - -// // Reboot is a mock method that overrides the embedded sequencer's Reboot method. -// func (s *MockUnsuccessfulSequencer) Reboot() []Phase { -// return []Phase{ -// &MockUnsuccessfulPhase{}, -// } -// } - -// // Reset is a mock method that overrides the embedded sequencer's Reset method. -// func (s *MockUnsuccessfulSequencer) Reset(req *machine.ResetRequest) []Phase { -// return []Phase{ -// &MockUnsuccessfulPhase{}, -// } -// } - -// type MockSuccessfulPhase struct{} - -// func (*MockSuccessfulPhase) Tasks() []TaskSetupFunc { -// return []TaskSetupFunc{&MockSuccessfulTask{}} -// } - -// type MockUnsuccessfulPhase struct{} - -// func (*MockUnsuccessfulPhase) Tasks() []TaskSetupFunc { -// return []TaskSetupFunc{&MockUnsuccessfulTask{}} -// } - -// type MockSuccessfulTask struct{} - -// func (*MockSuccessfulTask) Func(Mode) TaskSetupFunc { -// return func(Runtime) error { -// return nil -// } -// } - -// type MockUnsuccessfulTask struct{} - -// func (*MockUnsuccessfulTask) Func(Mode) TaskSetupFunc { -// return func(Runtime) error { return fmt.Errorf("error") } -// } - -// type MockPlatform struct{} - -// func (*MockPlatform) Name() string { -// return "mock" -// } - -// func (*MockPlatform) Configuration() ([]byte, error) { -// return nil, nil -// } - -// func (*MockPlatform) ExternalIPs() ([]net.IP, error) { -// return nil, nil -// } - -// func (*MockPlatform) Hostname() ([]byte, error) { -// return nil, nil -// } - -// func (*MockPlatform) Mode() Mode { -// return Metal -// } - -// func (*MockPlatform) KernelArgs() procfs.Parameters { -// return procfs.Parameters{} -// } - -// type MockConfigurator struct{} - -// func (*MockConfigurator) Version() string { -// return "" -// } - -// func (*MockConfigurator) Debug() bool { -// return false -// } - -// func (*MockConfigurator) Persist() bool { -// return false -// } - -// func (*MockConfigurator) Machine() Machine { -// return nil -// } - -// func (*MockConfigurator) Cluster() Cluster { -// return nil -// } - -// func (*MockConfigurator) Validate(Mode) error { -// return nil -// } - -// func (*MockConfigurator) String() (string, error) { -// return "", nil -// } - -// func (*MockConfigurator) Bytes() ([]byte, error) { -// return nil, nil -// } - -// type MockRuntime struct{} - -// func (*MockRuntime) Platform() Platform { -// return &MockPlatform{} -// } - -// func (*MockRuntime) Config() Configurator { -// return &MockConfigurator{} -// } - -// func (*MockRuntime) Sequence() Sequence { -// return Noop -// } - -// func TestController_Run(t *testing.T) { -// type fields struct { -// Sequencer Sequencer -// Runtime Runtime -// semaphore int32 -// } - -// type args struct { -// seq Sequence -// data interface{} -// } - -// tests := []struct { -// name string -// fields fields -// args args -// wantErr bool -// }{ -// { -// name: "boot", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Boot, -// data: nil, -// }, -// wantErr: false, -// }, -// { -// name: "initialize", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Initialize, -// data: nil, -// }, -// wantErr: false, -// }, -// { -// name: "shutdown", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Shutdown, -// data: nil, -// }, -// wantErr: false, -// }, -// { -// name: "upgrade with valid data", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Upgrade, -// data: &machine.UpgradeRequest{}, -// }, -// wantErr: false, -// }, -// { -// name: "upgrade with invalid data", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Upgrade, -// data: nil, -// }, -// wantErr: true, -// }, -// { -// name: "upgrade with lock", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 1, -// }, -// args: args{ -// seq: Upgrade, -// data: &machine.UpgradeRequest{}, -// }, -// wantErr: true, -// }, -// { -// name: "reset with valid data", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Reset, -// data: &machine.ResetRequest{}, -// }, -// wantErr: false, -// }, -// { -// name: "reset with invalid data", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Reset, -// data: nil, -// }, -// wantErr: true, -// }, -// { -// name: "unsuccessful phase", -// fields: fields{ -// Sequencer: &MockUnsuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// seq: Boot, -// data: nil, -// }, -// wantErr: true, -// }, -// { -// name: "undefined runtime", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: nil, -// semaphore: 0, -// }, -// args: args{ -// seq: Boot, -// data: nil, -// }, -// wantErr: true, -// }, -// } - -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// c := &Controller{ -// Sequencer: tt.fields.Sequencer, -// Runtime: tt.fields.Runtime, -// semaphore: tt.fields.semaphore, -// } -// t.Logf("c.Sequencer: %v", c.Sequencer) -// if err := c.Run(tt.args.seq, tt.args.data); (err != nil) != tt.wantErr { -// t.Errorf("Controller.Run() error = %v, wantErr %v", err, tt.wantErr) -// } -// }) -// } -// } - -// func TestController_runPhase(t *testing.T) { -// type fields struct { -// Sequencer Sequencer -// Runtime Runtime -// semaphore int32 -// } - -// type args struct { -// phase Phase -// } - -// tests := []struct { -// name string -// fields fields -// args args -// wantErr bool -// }{ -// { -// name: "successful phase", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// phase: &MockSuccessfulPhase{}, -// }, -// wantErr: false, -// }, -// { -// name: "unsuccessful phase", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// phase: &MockUnsuccessfulPhase{}, -// }, -// wantErr: true, -// }, -// } - -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// c := &Controller{ -// Sequencer: tt.fields.Sequencer, -// Runtime: tt.fields.Runtime, -// semaphore: tt.fields.semaphore, -// } -// if err := c.runPhase(tt.args.phase); (err != nil) != tt.wantErr { -// t.Errorf("Controller.runPhase() error = %v, wantErr %v", err, tt.wantErr) -// } -// }) -// } -// } - -// func TestController_runTask(t *testing.T) { -// type fields struct { -// Sequencer Sequencer -// Runtime Runtime -// semaphore int32 -// } - -// type args struct { -// t TaskSetupFunc -// } - -// tests := []struct { -// name string -// fields fields -// args args -// wantErr bool -// }{ -// { -// name: "successful task", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// t: &MockSuccessfulTask{}, -// }, -// wantErr: false, -// }, -// { -// name: "unsuccessful task", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// Runtime: &MockRuntime{}, -// semaphore: 0, -// }, -// args: args{ -// t: &MockUnsuccessfulTask{}, -// }, -// wantErr: true, -// }, -// } - -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// c := &Controller{ -// Sequencer: tt.fields.Sequencer, -// Runtime: tt.fields.Runtime, -// semaphore: tt.fields.semaphore, -// } - -// if err := c.runTask(tt.args.t); (err != nil) != tt.wantErr { -// t.Errorf("Controller.runTask() error = %v, wantErr %v", err, tt.wantErr) -// } -// }) -// } -// } - -// func TestController_TryLock(t *testing.T) { -// type fields struct { -// Sequencer Sequencer -// Runtime Runtime -// semaphore int32 -// } - -// tests := []struct { -// name string -// fields fields -// want bool -// }{ -// { -// name: "is locked", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// semaphore: 0, -// }, -// want: false, -// }, -// { -// name: "is unlocked", -// fields: fields{ -// Sequencer: &MockSuccessfulSequencer{}, -// semaphore: 1, -// }, -// want: true, -// }, -// } - -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// c := &Controller{ -// Sequencer: tt.fields.Sequencer, -// Runtime: tt.fields.Runtime, -// semaphore: tt.fields.semaphore, -// } -// if got := c.TryLock(); got != tt.want { -// t.Errorf("Controller.TryLock() = %v, want %v", got, tt.want) -// } -// }) -// } -// } - -// func TestController_Unlock(t *testing.T) { -// type fields struct { -// Sequencer Sequencer -// Runtime Runtime -// semaphore int32 -// } - -// tests := []struct { -// name string -// fields fields -// want bool -// }{ -// { -// name: "did not unlock", -// fields: fields{ -// semaphore: 0, -// }, -// want: false, -// }, -// { -// name: "did unlock", -// fields: fields{ -// semaphore: 1, -// }, -// want: true, -// }, -// } - -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// c := &Controller{ -// Sequencer: tt.fields.Sequencer, -// Runtime: tt.fields.Runtime, -// semaphore: tt.fields.semaphore, -// } -// if got := c.Unlock(); got != tt.want { -// t.Errorf("Controller.Unlock() = %v, want %v", got, tt.want) -// } -// }) -// } -// } - -// import ( -// "errors" -// "os" -// "testing" - -// "github.com/stretchr/testify/suite" - -// "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" -// "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/phase" -// ) - -// type PhaseSuite struct { -// suite.Suite - -// platformExists bool -// platformValue string -// } - -// type regularTask struct { -// errCh <-chan error -// } - -// func (t *regularTask) TaskFunc(runtime.Mode) TaskFunc { -// return func(runtime.Runtime) error { -// return <-t.errCh -// } -// } - -// type nilTask struct{} - -// func (t *nilTask) TaskFunc(runtime.Mode) TaskFunc { -// return nil -// } - -// type panicTask struct{} - -// func (t *panicTask) TaskFunc(runtime.Mode) TaskFunc { -// return func(runtime.Runtime) error { -// panic("in task") -// } -// } - -// func (suite *PhaseSuite) SetupSuite() { -// suite.platformValue, suite.platformExists = os.LookupEnv("PLATFORM") -// suite.Require().NoError(os.Setenv("PLATFORM", "container")) -// } - -// func (suite *PhaseSuite) TearDownSuite() { -// if !suite.platformExists { -// suite.Require().NoError(os.Unsetenv("PLATFORM")) -// } else { -// suite.Require().NoError(os.Setenv("PLATFORM", suite.platformValue)) -// } -// } - -// func (suite *PhaseSuite) TestRunSuccess() { -// r, err := phase.NewRunner(nil, runtime.Noop) -// suite.Require().NoError(err) - -// taskErr := make(chan error) - -// r.Add(phase.NewPhase("empty")) -// r.Add(phase.NewPhase("phase1", ®ularTask{errCh: taskErr}, ®ularTask{errCh: taskErr})) -// r.Add(phase.NewPhase("phase2", ®ularTask{errCh: taskErr}, &nilTask{})) - -// errCh := make(chan error) - -// go func() { -// errCh <- r.Run() -// }() - -// taskErr <- nil -// taskErr <- nil - -// select { -// case <-errCh: -// suite.Require().Fail("should be still running") -// default: -// } - -// taskErr <- nil - -// suite.Require().NoError(<-errCh) -// } - -// func (suite *PhaseSuite) TestRunFailures() { -// r, err := phase.NewRunner(nil, runtime.Noop) -// suite.Require().NoError(err) - -// taskErr := make(chan error, 1) - -// r.Add(phase.NewPhase("empty")) -// r.Add(phase.NewPhase("failphase", &panicTask{}, ®ularTask{errCh: taskErr}, &nilTask{})) -// r.Add(phase.NewPhase("neverreached", -// ®ularTask{}, // should never be reached -// )) - -// taskErr <- errors.New("test error") - -// err = r.Run() -// suite.Require().Error(err) -// suite.Assert().Contains(err.Error(), "2 errors occurred") -// suite.Assert().Contains(err.Error(), "test error") -// suite.Assert().Contains(err.Error(), "panic recovered: in task") -// } - -// func TestPhaseSuite(t *testing.T) { -// suite.Run(t, new(PhaseSuite)) -// } diff --git a/internal/app/machined/pkg/runtime/platform.go b/internal/app/machined/pkg/runtime/platform.go index 671c55823..e159a1471 100644 --- a/internal/app/machined/pkg/runtime/platform.go +++ b/internal/app/machined/pkg/runtime/platform.go @@ -6,17 +6,52 @@ package runtime import ( "context" - "net" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" + + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) // Platform defines the requirements for a platform. type Platform interface { + // Name returns platform name. Name() string - Configuration(context.Context) ([]byte, error) - Hostname(context.Context) ([]byte, error) + + // Mode returns platform mode (metal, cloud or container). Mode() Mode - ExternalIPs(context.Context) ([]net.IP, error) + + // Configuration fetches the machine configuration from platform-specific location. + // + // On cloud-like platform it is user-data in metadata service. + // For metal platform that is either `talos.config=` URL or mounted ISO image. + Configuration(context.Context) ([]byte, error) + + // KernelArgs returns additional kernel arguments which should be injected for the kernel boot. KernelArgs() procfs.Parameters + + // NetworkConfiguration fetches network configuration from the platform metadata. + // + // Controller will run this in function a separate goroutine, restarting it + // on error. Platform is expected to deliver network configuration over the channel, + // including updates to the configuration over time. + NetworkConfiguration(context.Context, chan<- *PlatformNetworkConfig) error +} + +// PlatformNetworkConfig describes the network configuration produced by the platform. +// +// This structure is marshaled to STATE partition to persist cached network configuration across +// reboots. +type PlatformNetworkConfig struct { + Addresses []network.AddressSpecSpec `yaml:"addresses"` + Links []network.LinkSpecSpec `yaml:"links"` + Routes []network.RouteSpecSpec `yaml:"routes"` + + Hostnames []network.HostnameSpecSpec `yaml:"hostnames"` + Resolvers []network.ResolverSpecSpec `yaml:"resolvers"` + TimeServers []network.TimeServerSpecSpec `yaml:"timeServers"` + + Operators []network.OperatorSpecSpec `yaml:"operators"` + + ExternalIPs []netaddr.IP `yaml:"externalIPs"` } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go index 0bd23eb35..aa72fa31c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/aws/aws.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "log" - "net" "strings" "github.com/aws/aws-sdk-go/aws" @@ -17,9 +16,11 @@ import ( "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const notFoundError = "NotFoundError" @@ -65,9 +66,7 @@ func (a *AWS) Configuration(ctx context.Context) ([]byte, error) { return nil, fmt.Errorf("failed to fetch EC2 userdata: %w", err) } - userdata = strings.TrimSpace(userdata) - - if userdata == "" { + if strings.TrimSpace(userdata) == "" { return nil, errors.ErrNoConfigSource } @@ -79,45 +78,70 @@ func (a *AWS) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (a *AWS) Hostname(ctx context.Context) (hostname []byte, err error) { - host, err := a.metadataClient.GetMetadataWithContext(ctx, "hostname") - if err != nil { - if awsErr, ok := err.(awserr.Error); ok { - if awsErr.Code() == notFoundError { - return nil, nil - } - } - - return nil, fmt.Errorf("failed to fetch hostname from IMDS: %w", err) - } - - return []byte(host), nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (a *AWS) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - publicIP, err := a.metadataClient.GetMetadataWithContext(ctx, "public-ipv4") - if err != nil { - if awsErr, ok := err.(awserr.Error); ok { - if awsErr.Code() == notFoundError { - return nil, nil - } - } - - return nil, fmt.Errorf("failed to fetch public IPv4 from IMDS: %w", err) - } - - if addr := net.ParseIP(publicIP); addr != nil { - addrs = append(addrs, addr) - } - - return addrs, nil -} - // KernelArgs implements the runtime.Platform interface. func (a *AWS) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty1").Append("ttyS0"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +// +//nolint:gocyclo +func (a *AWS) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + getMetadataKey := func(key string) (string, error) { + v, err := a.metadataClient.GetMetadataWithContext(ctx, key) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == notFoundError { + return "", nil + } + } + + return "", fmt.Errorf("failed to fetch %q from IMDS: %w", key, err) + } + + return v, nil + } + + networkConfig := &runtime.PlatformNetworkConfig{} + + hostname, err := getMetadataKey("hostname") + if err != nil { + return err + } + + if hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err = hostnameSpec.ParseFQDN(hostname); err != nil { + return err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + externalIP, err := getMetadataKey("public-ipv4") + if err != nil { + return err + } + + if externalIP != "" { + ip, err := netaddr.ParseIP(externalIP) + if err != nil { + return err + } + + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go index 60125e81f..62cdb420d 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go @@ -9,24 +9,22 @@ import ( "encoding/base64" "encoding/json" "encoding/xml" + stderrors "errors" "fmt" "io/ioutil" "log" - "net" "os" "path/filepath" "regexp" - "github.com/AlekSi/pointer" "github.com/talos-systems/go-procfs/procfs" "golang.org/x/sys/unix" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const ( @@ -71,50 +69,61 @@ func (a *Azure) Name() string { return "azure" } -// ConfigurationNetwork implements the network configuration interface. -func (a *Azure) ConfigurationNetwork(metadataNetworkConfig []byte, confProvider config.Provider) (config.Provider, error) { - var machineConfig *v1alpha1.Config +// ParseMetadata parses Azure network metadata into the platform network config. +// +//nolint:gocyclo +func (a *Azure) ParseMetadata(interfaceAddresses []NetworkConfig, host []byte) (*runtime.PlatformNetworkConfig, error) { + var networkConfig runtime.PlatformNetworkConfig - machineConfig, ok := confProvider.Raw().(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") + // hostname + if len(host) > 0 { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(string(host)); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } - - var interfaceAddresses []NetworkConfig - - if err := json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil { - return nil, err - } - - if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil { - for idx, iface := range interfaceAddresses { - device := &v1alpha1.Device{ - DeviceInterface: fmt.Sprintf("eth%d", idx), - DeviceDHCP: true, - DeviceDHCPOptions: &v1alpha1.DHCPOptions{DHCPIPv6: pointer.ToBool(true)}, + // external IP + for _, iface := range interfaceAddresses { + for _, ipv4addr := range iface.IPv4.IPAddresses { + if ip, err := netaddr.ParseIP(ipv4addr.PublicIPAddress); err == nil { + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) } + } - ipv6 := false - - for _, ipv6addr := range iface.IPv6.IPAddresses { - ipv6 = ipv6addr.PublicIPAddress != "" || ipv6addr.PrivateIPAddress != "" - } - - if ipv6 { - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, device) + for _, ipv6addr := range iface.IPv6.IPAddresses { + if ip, err := netaddr.ParseIP(ipv6addr.PublicIPAddress); err == nil { + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) } } } - return machineConfig, nil + // DHCP6 for enabled interfaces + for idx, iface := range interfaceAddresses { + ipv6 := false + + for _, ipv6addr := range iface.IPv6.IPAddresses { + ipv6 = ipv6addr.PublicIPAddress != "" || ipv6addr.PrivateIPAddress != "" + } + + if ipv6 { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP6, + LinkName: fmt.Sprintf("eth%d", idx), + RequireUp: true, + DHCP6: network.DHCP6OperatorSpec{ + RouteMetric: 1024, + }, + }) + } + } + + return &networkConfig, nil } // Configuration implements the platform.Platform interface. @@ -125,35 +134,10 @@ func (a *Azure) Configuration(ctx context.Context) ([]byte, error) { } }() - log.Printf("fetching network config from %q", AzureInterfacesEndpoint) - - metadataNetworkConfig, err := download.Download(ctx, AzureInterfacesEndpoint, - download.WithHeaders(map[string]string{"Metadata": "true"})) - if err != nil { - return nil, fmt.Errorf("failed to fetch network config from metadata service") - } - log.Printf("fetching machine config from ovf-env.xml") // Custom data is not available in IMDS, so trying to find it on CDROM. - machineConfig, err := a.configFromCD() - if err != nil { - log.Printf("fetching machine config from cdrom failed, err: %s", err.Error()) - - return nil, err - } - - confProvider, err := configloader.NewFromBytes(machineConfig) - if err != nil { - return nil, fmt.Errorf("error parsing machine config: %w", err) - } - - confProvider, err = a.ConfigurationNetwork(metadataNetworkConfig, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() + return a.configFromCD() } // Mode implements the platform.Platform interface. @@ -161,41 +145,6 @@ func (a *Azure) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the platform.Platform interface. -func (a *Azure) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", AzureHostnameEndpoint) - - host, err := download.Download(ctx, AzureHostnameEndpoint, - download.WithHeaders(map[string]string{"Metadata": "true"}), - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - return nil, err - } - - return host, nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (a *Azure) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - log.Printf("fetching externalIP from: %q", AzureInterfacesEndpoint) - - metadataNetworkConfig, err := download.Download(ctx, AzureInterfacesEndpoint, - download.WithHeaders(map[string]string{"Metadata": "true"}), - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil { - return nil, err - } - - addrs, err = a.getPublicIPs(metadataNetworkConfig) - if err != nil { - return nil, err - } - - return addrs, nil -} - // KernelArgs implements the runtime.Platform interface. func (a *Azure) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ @@ -259,27 +208,42 @@ func (a *Azure) configFromCD() ([]byte, error) { return nil, errors.ErrNoConfigSource } -// getPublicIPs parced network metadata response. -func (a *Azure) getPublicIPs(metadataNetworkConfig []byte) (addrs []net.IP, err error) { +// NetworkConfiguration implements the runtime.Platform interface. +func (a *Azure) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching network config from %q", AzureInterfacesEndpoint) + + metadataNetworkConfig, err := download.Download(ctx, AzureInterfacesEndpoint, + download.WithHeaders(map[string]string{"Metadata": "true"})) + if err != nil { + return fmt.Errorf("failed to fetch network config from metadata service: %w", err) + } + var interfaceAddresses []NetworkConfig if err = json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil { - return nil, errors.ErrNoExternalIPs + return err } - for _, iface := range interfaceAddresses { - for _, ipv4addr := range iface.IPv4.IPAddresses { - if ip := net.ParseIP(ipv4addr.PublicIPAddress); ip != nil { - addrs = append(addrs, ip) - } - } + log.Printf("fetching hostname from: %q", AzureHostnameEndpoint) - for _, ipv6addr := range iface.IPv6.IPAddresses { - if ip := net.ParseIP(ipv6addr.PublicIPAddress); ip != nil { - addrs = append(addrs, ip) - } - } + host, err := download.Download(ctx, AzureHostnameEndpoint, + download.WithHeaders(map[string]string{"Metadata": "true"}), + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return err } - return addrs, nil + networkConfig, err := a.ParseMetadata(interfaceAddresses, host) + if err != nil { + return err + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go index e9bafc1a7..ba6e9a7a7 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure_test.go @@ -5,73 +5,35 @@ package azure_test import ( + _ "embed" + "encoding/json" "testing" - "github.com/AlekSi/pointer" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/azure" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} +//go:embed metadata.json +var rawMetadata []byte -func (suite *ConfigSuite) TestNetworkConfig() { - cfg := []byte(` -[ - { - "ipv4": { - "ipAddress": [ - { - "privateIpAddress": "172.18.1.10", - "publicIpAddress": "1.2.3.4" - } - ], - "subnet": [ - { - "address": "172.18.1.0", - "prefix": "24" - } - ] - }, - "ipv6": { - "ipAddress": [ - { - "privateIpAddress": "fd00::10", - "publicIpAddress": "" - } - ] - }, - "macAddress": "000D3AD631EE" - } -] -`) +//go:embed expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { a := &azure.Azure{} - defaultMachineConfig := &v1alpha1.Config{} + var m []azure.NetworkConfig - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: true, - DeviceDHCPOptions: &v1alpha1.DHCPOptions{DHCPIPv6: pointer.ToBool(true)}, - }, - }, - }, - }, - } + require.NoError(t, json.Unmarshal(rawMetadata, &m)) - result, err := a.ConfigurationNetwork(cfg, defaultMachineConfig) + networkConfig, err := a.ParseMetadata(m, []byte("some.fqdn")) + require.NoError(t, err) - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/expected.yaml new file mode 100644 index 000000000..cc11f07f9 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/expected.yaml @@ -0,0 +1,18 @@ +addresses: [] +links: [] +routes: [] +hostnames: + - hostname: some + domainname: fqdn + layer: platform +resolvers: [] +timeServers: [] +operators: + - operator: dhcp6 + linkName: eth0 + requireUp: true + dhcp6: + routeMetric: 1024 + layer: default +externalIPs: + - 1.2.3.4 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.json new file mode 100644 index 000000000..163ed0d13 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/azure/metadata.json @@ -0,0 +1,27 @@ +[ + { + "ipv4": { + "ipAddress": [ + { + "privateIpAddress": "172.18.1.10", + "publicIpAddress": "1.2.3.4" + } + ], + "subnet": [ + { + "address": "172.18.1.0", + "prefix": "24" + } + ] + }, + "ipv6": { + "ipAddress": [ + { + "privateIpAddress": "fd00::10", + "publicIpAddress": "" + } + ] + }, + "macAddress": "000D3AD631EE" + } +] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go index 9d335b09f..b97db4bf9 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/container/container.go @@ -10,13 +10,13 @@ import ( "encoding/base64" "io/ioutil" "log" - "net" "os" "github.com/talos-systems/go-procfs/procfs" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) // Container is a platform for installing Talos via an Container image. @@ -44,28 +44,40 @@ func (c *Container) Configuration(context.Context) ([]byte, error) { return decoded, nil } -// Hostname implements the platform.Platform interface. -func (c *Container) Hostname(context.Context) (hostname []byte, err error) { - hostname, err = ioutil.ReadFile("/etc/hostname") - - if err == nil { - hostname = bytes.TrimSpace(hostname) - } - - return -} - // Mode implements the platform.Platform interface. func (c *Container) Mode() runtime.Mode { return runtime.ModeContainer } -// ExternalIPs implements the runtime.Platform interface. -func (c *Container) ExternalIPs(context.Context) (addrs []net.IP, err error) { - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (c *Container) KernelArgs() procfs.Parameters { return nil } + +// NetworkConfiguration implements the runtime.Platform interface. +func (c *Container) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + networkConfig := &runtime.PlatformNetworkConfig{} + + hostname, err := ioutil.ReadFile("/etc/hostname") + if err != nil { + return err + } + + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(string(bytes.TrimSpace(hostname))); err != nil { + return err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go index 969cf8e6c..97b0aa15b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean/digitalocean.go @@ -6,17 +6,16 @@ package digitalocean import ( "context" - "fmt" - "io/ioutil" + stderrors "errors" "log" - "net" - "net/http" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const ( @@ -50,65 +49,56 @@ func (d *DigitalOcean) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the platform.Platform interface. -func (d *DigitalOcean) Hostname(ctx context.Context) (hostname []byte, err error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, DigitalOceanHostnameEndpoint, nil) - if err != nil { - return nil, err - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - //nolint:errcheck - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return hostname, fmt.Errorf("failed to fetch hostname from metadata service: %d", resp.StatusCode) - } - - return ioutil.ReadAll(resp.Body) -} - -// ExternalIPs implements the runtime.Platform interface. -func (d *DigitalOcean) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - var ( - body []byte - req *http.Request - resp *http.Response - ) - - if req, err = http.NewRequestWithContext(ctx, "GET", DigitalOceanExternalIPEndpoint, nil); err != nil { - return - } - - client := &http.Client{} - if resp, err = client.Do(req); err != nil { - return - } - - //nolint:errcheck - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return addrs, fmt.Errorf("failed to retrieve external addresses for instance") - } - - if body, err = ioutil.ReadAll(resp.Body); err != nil { - return - } - - if addr := net.ParseIP(string(body)); addr != nil { - addrs = append(addrs, addr) - } - - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (d *DigitalOcean) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("ttyS0").Append("tty0").Append("tty1"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +// +//nolint:gocyclo +func (d *DigitalOcean) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + host, err := download.Download(ctx, DigitalOceanHostnameEndpoint, + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return err + } + + extIP, err := download.Download(ctx, DigitalOceanExternalIPEndpoint, + download.WithErrorOnNotFound(errors.ErrNoExternalIPs), + download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) + if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { + return err + } + + networkConfig := &runtime.PlatformNetworkConfig{} + + if len(host) > 0 { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(string(host)); err != nil { + return err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + if len(extIP) > 0 { + if ip, err := netaddr.ParseIP(string(extIP)); err == nil { + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) + } + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go index 50dbe73ca..091354e7a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp/gcp.go @@ -6,14 +6,15 @@ package gcp import ( "context" - "net" "strings" "cloud.google.com/go/compute/metadata" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) // GCP is the concrete type that implements the platform.Platform interface. @@ -35,51 +36,67 @@ func (g *GCP) Configuration(ctx context.Context) ([]byte, error) { return nil, err } - userdata = strings.TrimSpace(userdata) - - if userdata == "" { + if strings.TrimSpace(userdata) == "" { return nil, errors.ErrNoConfigSource } return []byte(userdata), nil } -// Hostname implements the platform.Platform interface. -func (g *GCP) Hostname(context.Context) (hostname []byte, err error) { - host, err := metadata.Hostname() - if err != nil { - return nil, err - } - - return []byte(host), nil -} - // Mode implements the platform.Platform interface. func (g *GCP) Mode() runtime.Mode { return runtime.ModeCloud } -// ExternalIPs implements the runtime.Platform interface. -func (g *GCP) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - extIP, err := metadata.ExternalIP() - if err != nil { - if _, ok := err.(metadata.NotDefinedError); ok { - return nil, nil - } - - return nil, err - } - - if addr := net.ParseIP(extIP); addr != nil { - addrs = append(addrs, addr) - } - - return addrs, nil -} - // KernelArgs implements the runtime.Platform interface. func (g *GCP) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("ttyS0"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +func (g *GCP) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + networkConfig := &runtime.PlatformNetworkConfig{} + + hostname, err := metadata.Hostname() + if err != nil { + return err + } + + if hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err = hostnameSpec.ParseFQDN(hostname); err != nil { + return err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + externalIP, err := metadata.ExternalIP() + if err != nil { + if _, ok := err.(metadata.NotDefinedError); !ok { + return err + } + } + + if externalIP != "" { + ip, err := netaddr.ParseIP(externalIP) + if err != nil { + return err + } + + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go index 2e412ca5c..3fb4926c5 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go @@ -6,19 +6,19 @@ package hcloud import ( "context" + stderrors "errors" "fmt" "log" - "net" "github.com/talos-systems/go-procfs/procfs" "gopkg.in/yaml.v3" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const ( @@ -61,104 +61,109 @@ func (h *Hcloud) Name() string { return "hcloud" } -// ConfigurationNetwork implements the network configuration interface. +// ParseMetadata converts HCloud metadata to platform network configuration. +// //nolint:gocyclo -func (h *Hcloud) ConfigurationNetwork(metadataNetworkConfig []byte, confProvider config.Provider) (config.Provider, error) { - var unmarshalledNetworkConfig NetworkConfig +func (h *Hcloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, host, extIP []byte) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - if err := yaml.Unmarshal(metadataNetworkConfig, &unmarshalledNetworkConfig); err != nil { - return nil, err + if len(host) > 0 { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(string(host)); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if unmarshalledNetworkConfig.Version != 1 { - return nil, fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version) + if len(extIP) > 0 { + if ip, err := netaddr.ParseIP(string(extIP)); err == nil { + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) + } } - var machineConfig *v1alpha1.Config - - machineConfig, ok := confProvider.Raw().(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") - } - - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } - - for _, network := range unmarshalledNetworkConfig.Config { - if network.Type != "physical" { + for _, ntwrk := range unmarshalledNetworkConfig.Config { + if ntwrk.Type != "physical" { continue } - iface := v1alpha1.Device{ - DeviceInterface: network.Interfaces, - DeviceDHCP: false, - } + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: ntwrk.Interfaces, + Up: true, + ConfigLayer: network.ConfigPlatform, + }) - for _, subnet := range network.Subnets { + for _, subnet := range ntwrk.Subnets { if subnet.Type == "dhcp" && subnet.Ipv4 { - iface.DeviceDHCP = true + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: ntwrk.Interfaces, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) } if subnet.Type == "static" { - iface.DeviceAddresses = append(iface.DeviceAddresses, - subnet.Address, + ipAddr, err := netaddr.ParseIPPrefix(subnet.Address) + if err != nil { + return nil, err + } + + family := nethelpers.FamilyInet4 + if ipAddr.IP().Is6() { + family = nethelpers.FamilyInet6 + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: ntwrk.Interfaces, + Address: ipAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }, ) } if subnet.Gateway != "" && subnet.Ipv6 { - iface.DeviceRoutes = []*v1alpha1.Route{ - { - RouteNetwork: "::/0", - RouteGateway: subnet.Gateway, - RouteMetric: 1024, - }, + gw, err := netaddr.ParseIP(subnet.Gateway) + if err != nil { + return nil, err } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: ntwrk.Interfaces, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet6, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) } } - - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append( - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, - &iface, - ) } - return machineConfig, nil + return networkConfig, nil } // Configuration implements the runtime.Platform interface. func (h *Hcloud) Configuration(ctx context.Context) ([]byte, error) { - log.Printf("fetching hcloud network config from: %q", HCloudNetworkEndpoint) - - metadataNetworkConfig, err := download.Download(ctx, HCloudNetworkEndpoint) - if err != nil { - return nil, fmt.Errorf("failed to fetch network config from metadata service") - } - log.Printf("fetching machine config from: %q", HCloudUserDataEndpoint) - machineConfigDl, err := download.Download(ctx, HCloudUserDataEndpoint, + return download.Download(ctx, HCloudUserDataEndpoint, download.WithErrorOnNotFound(errors.ErrNoConfigSource), download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, err - } - - confProvider, err := configloader.NewFromBytes(machineConfigDl) - if err != nil { - return nil, err - } - - confProvider, err = h.ConfigurationNetwork(metadataNetworkConfig, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() } // Mode implements the runtime.Platform interface. @@ -166,41 +171,62 @@ func (h *Hcloud) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (h *Hcloud) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", HCloudHostnameEndpoint) - - host, err := download.Download(ctx, HCloudHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - return nil, err - } - - return host, nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (h *Hcloud) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - log.Printf("fetching externalIP from: %q", HCloudExternalIPEndpoint) - - exIP, err := download.Download(ctx, HCloudExternalIPEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil { - return nil, err - } - - if ip := net.ParseIP(string(exIP)); ip != nil { - addrs = append(addrs, ip) - } - - return addrs, nil -} - // KernelArgs implements the runtime.Platform interface. func (h *Hcloud) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty1").Append("ttyS0"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +// +//nolint:gocyclo +func (h *Hcloud) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching hcloud network config from: %q", HCloudNetworkEndpoint) + + metadataNetworkConfig, err := download.Download(ctx, HCloudNetworkEndpoint) + if err != nil { + return fmt.Errorf("failed to fetch network config from metadata service: %w", err) + } + + var unmarshalledNetworkConfig NetworkConfig + + if err = yaml.Unmarshal(metadataNetworkConfig, &unmarshalledNetworkConfig); err != nil { + return err + } + + if unmarshalledNetworkConfig.Version != 1 { + return fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version) + } + + log.Printf("fetching hostname from: %q", HCloudHostnameEndpoint) + + host, err := download.Download(ctx, HCloudHostnameEndpoint, + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil && !stderrors.Is(err, errors.ErrNoHostname) { + return err + } + + log.Printf("fetching externalIP from: %q", HCloudExternalIPEndpoint) + + extIP, err := download.Download(ctx, HCloudExternalIPEndpoint, + download.WithErrorOnNotFound(errors.ErrNoExternalIPs), + download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) + if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { + return err + } + + networkConfig, err := h.ParseMetadata(&unmarshalledNetworkConfig, host, extIP) + if err != nil { + return err + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go index 8957109fd..e13a90075 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud_test.go @@ -5,69 +5,37 @@ package hcloud_test import ( + _ "embed" + "fmt" "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} - -func (suite *ConfigSuite) TestNetworkConfig() { - cfg := []byte(` -config: -- mac_address: 96:00:00:1:2:3 - name: eth0 - subnets: - - ipv4: true - type: dhcp - - address: 2a01:4f8:1:2::1/64 - gateway: fe80::1 - ipv6: true - type: static - type: physical -- address: - - 185.12.64.2 - - 185.12.64.1 - interface: eth0 - type: nameserver -version: 1 -`) - p := &hcloud.Hcloud{} - - defaultMachineConfig := &v1alpha1.Config{} - - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: true, - DeviceAddresses: []string{"2a01:4f8:1:2::1/64"}, - DeviceRoutes: []*v1alpha1.Route{ - { - RouteNetwork: "::/0", - RouteGateway: "fe80::1", - RouteMetric: 1024, - }, - }, - }, - }, - }, - }, - } - - result, err := p.ConfigurationNetwork(cfg, defaultMachineConfig) - - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) +//go:embed testdata/metadata.yaml +var rawMetadata []byte + +//go:embed testdata/expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { + h := &hcloud.Hcloud{} + + var m hcloud.NetworkConfig + + require.NoError(t, yaml.Unmarshal(rawMetadata, &m)) + + networkConfig, err := h.ParseMetadata(&m, []byte("some.fqdn"), []byte("1.2.3.4")) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + fmt.Print(string(marshaled)) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml new file mode 100644 index 000000000..b6ae357c3 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/expected.yaml @@ -0,0 +1,42 @@ +addresses: + - address: 2a01:4f8:1:2::1/64 + linkName: eth0 + family: inet6 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +routes: + - family: inet6 + dst: "" + src: "" + gateway: fe80::1 + outLinkName: eth0 + table: main + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: + - hostname: some + domainname: fqdn + layer: platform +resolvers: [] +timeServers: [] +operators: + - operator: dhcp4 + linkName: eth0 + requireUp: false + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: + - 1.2.3.4 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/metadata.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/metadata.yaml new file mode 100644 index 000000000..127b7d01d --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/testdata/metadata.yaml @@ -0,0 +1,17 @@ +config: +- mac_address: 96:00:00:1:2:3 + name: eth0 + subnets: + - ipv4: true + type: dhcp + - address: 2a01:4f8:1:2::1/64 + gateway: fe80::1 + ipv6: true + type: static + type: physical +- address: + - 185.12.64.2 + - 185.12.64.1 + interface: eth0 + type: nameserver +version: 1 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go index 3d46a678d..45822e02c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/metal/metal.go @@ -9,7 +9,6 @@ import ( "fmt" "io/ioutil" "log" - "net" "net/url" "path/filepath" "strings" @@ -105,21 +104,11 @@ func getSystemUUID() (uuid.UUID, error) { return s.SystemInformation().UUID() } -// Hostname implements the platform.Platform interface. -func (m *Metal) Hostname(context.Context) (hostname []byte, err error) { - return nil, nil -} - // Mode implements the platform.Platform interface. func (m *Metal) Mode() runtime.Mode { return runtime.ModeMetal } -// ExternalIPs implements the platform.Platform interface. -func (m *Metal) ExternalIPs(context.Context) (addrs []net.IP, err error) { - return addrs, err -} - func readConfigFromISO() (b []byte, err error) { var dev *probe.ProbedBlockDevice @@ -162,3 +151,8 @@ func (m *Metal) KernelArgs() procfs.Parameters { procfs.NewParameter("console").Append("ttyS0").Append("tty0"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +func (m *Metal) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go new file mode 100644 index 000000000..38780c674 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/metadata.go @@ -0,0 +1,468 @@ +// 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 nocloud + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net/url" + "path/filepath" + "strings" + + "github.com/talos-systems/go-blockdevice/blockdevice/filesystem" + "github.com/talos-systems/go-blockdevice/blockdevice/probe" + "github.com/talos-systems/go-smbios/smbios" + "golang.org/x/sys/unix" + "inet.af/netaddr" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/download" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" +) + +const ( + configISOLabel = "cidata" + configNetworkConfigPath = "network-config" + configMetaDataPath = "meta-data" + configUserDataPath = "user-data" + mnt = "/mnt" +) + +// NetworkConfig holds network-config info. +type NetworkConfig struct { + Version int `yaml:"version"` + Config []struct { + Mac string `yaml:"mac_address,omitempty"` + Interfaces string `yaml:"name,omitempty"` + MTU string `yaml:"mtu,omitempty"` + Subnets []struct { + Address string `yaml:"address,omitempty"` + Netmask string `yaml:"netmask,omitempty"` + Gateway string `yaml:"gateway,omitempty"` + Type string `yaml:"type"` + } `yaml:"subnets,omitempty"` + Address []string `yaml:"address,omitempty"` + Type string `yaml:"type"` + } `yaml:"config,omitempty"` + Ethernets map[string]Ethernet `yaml:"ethernets,omitempty"` + Bonds map[string]Bonds `yaml:"bonds,omitempty"` +} + +// Ethernet holds network interface info. +type Ethernet struct { + Match struct { + Name []string `yaml:"name,omitempty"` + HWAddr []string `yaml:"macaddress,omitempty"` + } `yaml:"match,omitempty"` + DHCPv4 bool `yaml:"dhcp4,omitempty"` + DHCPv6 bool `yaml:"dhcp6,omitempty"` + Address []string `yaml:"addresses,omitempty"` + Gateway4 string `yaml:"gateway4,omitempty"` + Gateway6 string `yaml:"gateway6,omitempty"` + MTU int `yaml:"mtu,omitempty"` + NameServers struct { + Search []string `yaml:"search,omitempty"` + Address []string `yaml:"addresses,omitempty"` + } `yaml:"nameservers,omitempty"` +} + +// Bonds holds bonding interface info. +type Bonds struct { + Interfaces []string `yaml:"interfaces,omitempty"` + Address []string `yaml:"addresses,omitempty"` + NameServers struct { + Search []string `yaml:"search,omitempty"` + Address []string `yaml:"addresses,omitempty"` + } `yaml:"nameservers,omitempty"` + Params []struct { + Mode string `yaml:"mode,omitempty"` + LACPRate string `yaml:"lacp-rate,omitempty"` + HashPolicy string `yaml:"transmit-hash-policy,omitempty"` + } `yaml:"parameters,omitempty"` +} + +// MetadataConfig holds meta info. +type MetadataConfig struct { + Hostname string `yaml:"hostname,omitempty"` + InstanceID string `yaml:"instance-id,omitempty"` +} + +func (n *Nocloud) configFromNetwork(ctx context.Context, metaBaseURL string) (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { + log.Printf("fetching meta config from: %q", metaBaseURL+configMetaDataPath) + + metaConfig, err = download.Download(ctx, metaBaseURL+configMetaDataPath) + if err != nil { + metaConfig = nil + } + + log.Printf("fetching network config from: %q", metaBaseURL+configNetworkConfigPath) + + networkConfig, err = download.Download(ctx, metaBaseURL+configNetworkConfigPath) + if err != nil { + networkConfig = nil + } + + log.Printf("fetching machine config from: %q", metaBaseURL+configUserDataPath) + + machineConfig, err = download.Download(ctx, metaBaseURL+configUserDataPath, + download.WithErrorOnNotFound(errors.ErrNoConfigSource), + download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) + + return metaConfig, networkConfig, machineConfig, err +} + +func (n *Nocloud) configFromCD() (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { + var dev *probe.ProbedBlockDevice + + dev, err = probe.GetDevWithFileSystemLabel(strings.ToLower(configISOLabel)) + if err != nil { + dev, err = probe.GetDevWithFileSystemLabel(strings.ToUpper(configISOLabel)) + if err != nil { + return nil, nil, nil, errors.ErrNoConfigSource + } + } + + //nolint:errcheck + defer dev.Close() + + sb, err := filesystem.Probe(dev.Path) + if err != nil || sb == nil { + return nil, nil, nil, errors.ErrNoConfigSource + } + + log.Printf("found config disk (cidata) at %s", dev.Path) + + if err = unix.Mount(dev.Path, mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil { + return nil, nil, nil, errors.ErrNoConfigSource + } + + log.Printf("fetching meta config from: cidata/%s", configMetaDataPath) + + metaConfig, err = ioutil.ReadFile(filepath.Join(mnt, configMetaDataPath)) + if err != nil { + log.Printf("failed to read %s", configMetaDataPath) + + metaConfig = nil + } + + log.Printf("fetching network config from: cidata/%s", configNetworkConfigPath) + + networkConfig, err = ioutil.ReadFile(filepath.Join(mnt, configNetworkConfigPath)) + if err != nil { + log.Printf("failed to read %s", configNetworkConfigPath) + + networkConfig = nil + } + + log.Printf("fetching machine config from: cidata/%s", configUserDataPath) + + machineConfig, err = ioutil.ReadFile(filepath.Join(mnt, configUserDataPath)) + if err != nil { + log.Printf("failed to read %s", configUserDataPath) + + machineConfig = nil + } + + if err = unix.Unmount(mnt, 0); err != nil { + return nil, nil, nil, fmt.Errorf("failed to unmount: %w", err) + } + + return metaConfig, networkConfig, machineConfig, nil +} + +//nolint:gocyclo +func (n *Nocloud) acquireConfig(ctx context.Context) (metadataConfigDl, metadataNetworkConfigDl, machineConfigDl []byte, hostname string, err error) { + s, err := smbios.New() + if err != nil { + return nil, nil, nil, "", err + } + + metaBaseURL := "" + networkSource := false + + options := strings.Split(s.SystemInformation().SerialNumber(), ";") + for _, option := range options { + parts := strings.SplitN(option, "=", 2) + if len(parts) == 2 { + switch parts[0] { + case "ds": + if parts[1] == "nocloud-net" { + networkSource = true + } + case "s": + var u *url.URL + + u, err = url.Parse(parts[1]) + if err == nil && strings.HasPrefix(u.Scheme, "http") { + if strings.HasSuffix(u.Path, "/") { + metaBaseURL = parts[1] + } else { + metaBaseURL = parts[1] + "/" + } + } + case "h": + hostname = parts[1] + } + } + } + + if networkSource && metaBaseURL != "" { + metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromNetwork(ctx, metaBaseURL) + } else { + metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromCD() + } + + return metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, hostname, err +} + +//nolint:gocyclo +func (n *Nocloud) applyNetworkConfigV1(config *NetworkConfig, networkConfig *runtime.PlatformNetworkConfig) error { + for _, ntwrk := range config.Config { + switch ntwrk.Type { + case "nameserver": + dnsIPs := make([]netaddr.IP, 0, len(ntwrk.Address)) + + for i := range ntwrk.Address { + if ip, err := netaddr.ParseIP(ntwrk.Address[i]); err == nil { + dnsIPs = append(dnsIPs, ip) + } else { + return err + } + } + + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + DNSServers: dnsIPs, + ConfigLayer: network.ConfigPlatform, + }) + case "physical": + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: ntwrk.Interfaces, + Up: true, + ConfigLayer: network.ConfigPlatform, + }) + + for _, subnet := range ntwrk.Subnets { + switch subnet.Type { + case "dhcp", "dhcp4": + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: ntwrk.Interfaces, + RequireUp: true, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + case "static", "static6": + family := nethelpers.FamilyInet4 + + if subnet.Type == "static6" { + family = nethelpers.FamilyInet6 + } + + ipPrefix, err := netaddr.ParseIPPrefix(subnet.Address) + if err != nil { + ip, err := netaddr.ParseIP(subnet.Address) + if err != nil { + return err + } + + netmask, err := netaddr.ParseIP(subnet.Netmask) + if err != nil { + return err + } + + mask := netmask.As4() + + ipPrefix, err = ip.Netmask(mask[:]) + if err != nil { + return err + } + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: ntwrk.Interfaces, + Address: ipPrefix, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }, + ) + + if subnet.Gateway != "" { + gw, err := netaddr.ParseIP(subnet.Gateway) + if err != nil { + return err + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: ntwrk.Interfaces, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + Priority: 1024, + } + + if family == nethelpers.FamilyInet6 { + route.Priority = 2048 + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + case "ipv6_dhcpv6-stateful": + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP6, + LinkName: ntwrk.Interfaces, + RequireUp: true, + DHCP6: network.DHCP6OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + } + } + } + } + + return nil +} + +//nolint:gocyclo +func (n *Nocloud) applyNetworkConfigV2(config *NetworkConfig, networkConfig *runtime.PlatformNetworkConfig) error { + var dnsIPs []netaddr.IP + + for name, eth := range config.Ethernets { + if !strings.HasPrefix(name, "eth") { + continue + } + + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: name, + Up: true, + MTU: uint32(eth.MTU), + ConfigLayer: network.ConfigPlatform, + }) + + if eth.DHCPv4 { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: name, + RequireUp: true, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + } + + if eth.DHCPv6 { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP6, + LinkName: name, + RequireUp: true, + DHCP6: network.DHCP6OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + } + + for _, addr := range eth.Address { + ipPrefix, err := netaddr.ParseIPPrefix(addr) + if err != nil { + return err + } + + family := nethelpers.FamilyInet4 + + if ipPrefix.IP().Is6() { + family = nethelpers.FamilyInet6 + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: name, + Address: ipPrefix, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }, + ) + } + + if eth.Gateway4 != "" { + gw, err := netaddr.ParseIP(eth.Gateway4) + if err != nil { + return err + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: name, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet4, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + + if eth.Gateway6 != "" { + gw, err := netaddr.ParseIP(eth.Gateway6) + if err != nil { + return err + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: name, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet6, + Priority: 2048, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + + for _, addr := range eth.NameServers.Address { + if ip, err := netaddr.ParseIP(addr); err == nil { + dnsIPs = append(dnsIPs, ip) + } else { + return err + } + } + } + + if len(dnsIPs) > 0 { + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + DNSServers: dnsIPs, + ConfigLayer: network.ConfigPlatform, + }) + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go index d90a998ab..55477b110 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud.go @@ -7,97 +7,17 @@ package nocloud import ( "bytes" "context" + stderrors "errors" "fmt" - "io/ioutil" - "log" - "net" - "net/url" - "path/filepath" - "strings" - "github.com/AlekSi/pointer" - "github.com/talos-systems/go-blockdevice/blockdevice/filesystem" - "github.com/talos-systems/go-blockdevice/blockdevice/probe" "github.com/talos-systems/go-procfs/procfs" - "github.com/talos-systems/go-smbios/smbios" - "golang.org/x/sys/unix" yaml "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" - "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) -const ( - configISOLabel = "cidata" - configNetworkConfigPath = "network-config" - configMetaDataPath = "meta-data" - configUserDataPath = "user-data" - mnt = "/mnt" -) - -// NetworkConfig holds network-config info. -type NetworkConfig struct { - Version int `yaml:"version"` - Config []struct { - Mac string `yaml:"mac_address,omitempty"` - Interfaces string `yaml:"name,omitempty"` - MTU string `yaml:"mtu,omitempty"` - Subnets []struct { - Address string `yaml:"address,omitempty"` - Netmask string `yaml:"netmask,omitempty"` - Gateway string `yaml:"gateway,omitempty"` - Type string `yaml:"type"` - } `yaml:"subnets,omitempty"` - Address []string `yaml:"address,omitempty"` - Type string `yaml:"type"` - } `yaml:"config,omitempty"` - Ethernets map[string]Ethernet `yaml:"ethernets,omitempty"` - Bonds map[string]Bonds `yaml:"bonds,omitempty"` -} - -// Ethernet holds network interface info. -type Ethernet struct { - Match struct { - Name []string `yaml:"name,omitempty"` - HWAddr []string `yaml:"macaddress,omitempty"` - } `yaml:"match,omitempty"` - DHCPv4 bool `yaml:"dhcp4,omitempty"` - DHCPv6 bool `yaml:"dhcp6,omitempty"` - Address []string `yaml:"addresses,omitempty"` - Gateway4 string `yaml:"gateway4,omitempty"` - Gateway6 string `yaml:"gateway6,omitempty"` - MTU int `yaml:"mtu,omitempty"` - NameServers struct { - Search []string `yaml:"search,omitempty"` - Address []string `yaml:"addresses,omitempty"` - } `yaml:"nameservers,omitempty"` -} - -// Bonds holds bonding interface info. -type Bonds struct { - Interfaces []string `yaml:"interfaces,omitempty"` - Address []string `yaml:"addresses,omitempty"` - NameServers struct { - Search []string `yaml:"search,omitempty"` - Address []string `yaml:"addresses,omitempty"` - } `yaml:"nameservers,omitempty"` - Params []struct { - Mode string `yaml:"mode,omitempty"` - LACPRate string `yaml:"lacp-rate,omitempty"` - HashPolicy string `yaml:"transmit-hash-policy,omitempty"` - } `yaml:"parameters,omitempty"` -} - -// MetadataConfig holds meta info. -type MetadataConfig struct { - Hostname string `yaml:"hostname,omitempty"` - InstanceID string `yaml:"instance-id,omitempty"` -} - // Nocloud is the concrete type that implements the runtime.Platform interface. type Nocloud struct{} @@ -106,135 +26,50 @@ func (n *Nocloud) Name() string { return "nocloud" } -// ConfigurationNetwork implements the network configuration interface. -//nolint:gocyclo -func (n *Nocloud) ConfigurationNetwork(metadataNetworkConfig []byte, metadataConfig []byte, confProvider config.Provider) (config.Provider, error) { - var ( - unmarshalledMetadataConfig MetadataConfig - machineConfig *v1alpha1.Config - ) +// ParseMetadata converts nocloud metadata to platform network config. +func (n *Nocloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, hostname string) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - if err := yaml.Unmarshal(metadataConfig, &unmarshalledMetadataConfig); err != nil { - unmarshalledMetadataConfig = MetadataConfig{} - } + if hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } - machineConfig, ok := confProvider.(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") - } - - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork.NetworkHostname == "" && unmarshalledMetadataConfig.Hostname != "" { - machineConfig.MachineConfig.MachineNetwork.NetworkHostname = unmarshalledMetadataConfig.Hostname - } - - if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil { - var unmarshalledNetworkConfig NetworkConfig - - if err := yaml.Unmarshal(metadataNetworkConfig, &unmarshalledNetworkConfig); err != nil { + if err := hostnameSpec.ParseFQDN(hostname); err != nil { return nil, err } - switch unmarshalledNetworkConfig.Version { - case 1: - n.applyNetworkConfigV1(unmarshalledNetworkConfig, machineConfig) - case 2: - n.applyNetworkConfigV2(unmarshalledNetworkConfig, machineConfig) - default: - return nil, fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version) - } + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - return confProvider, nil + switch unmarshalledNetworkConfig.Version { + case 1: + if err := n.applyNetworkConfigV1(unmarshalledNetworkConfig, networkConfig); err != nil { + return nil, err + } + case 2: + if err := n.applyNetworkConfigV2(unmarshalledNetworkConfig, networkConfig); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version) + } + + return networkConfig, nil } // Configuration implements the runtime.Platform interface. -//nolint:gocyclo func (n *Nocloud) Configuration(ctx context.Context) ([]byte, error) { - s, err := smbios.New() + _, _, machineConfigDl, _, err := n.acquireConfig(ctx) //nolint:dogsled if err != nil { return nil, err } - hostname := "" - metaBaseURL := "" - networkSource := false - - options := strings.Split(s.SystemInformation().SerialNumber(), ";") - for _, option := range options { - parts := strings.SplitN(option, "=", 2) - if len(parts) == 2 { - switch parts[0] { - case "ds": - if parts[1] == "nocloud-net" { - networkSource = true - } - case "s": - var u *url.URL - - u, err = url.Parse(parts[1]) - if err == nil && strings.HasPrefix(u.Scheme, "http") { - if strings.HasSuffix(u.Path, "/") { - metaBaseURL = parts[1] - } else { - metaBaseURL = parts[1] + "/" - } - } - case "h": - hostname = parts[1] - } - } - } - - var ( - metadataConfigDl []byte - metadataNetworkConfigDl []byte - machineConfigDl []byte - ) - - if networkSource && metaBaseURL != "" { - metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromNetwork(ctx, metaBaseURL) - if err != nil { - return nil, err - } - } else { - metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromCD() - if err != nil { - return nil, err - } - } - - if hostname != "" && metadataConfigDl == nil { - meta := &MetadataConfig{ - Hostname: hostname, - } - - //nolint:errcheck - metadataConfigDl, _ = yaml.Marshal(meta) - } - if bytes.HasPrefix(machineConfigDl, []byte("#cloud-config")) { return nil, errors.ErrNoConfigSource } - confProvider, err := configloader.NewFromBytes(machineConfigDl) - if err != nil { - return nil, err - } - - confProvider, err = n.ConfigurationNetwork(metadataNetworkConfigDl, metadataConfigDl, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() + return machineConfigDl, nil } // Mode implements the runtime.Platform interface. @@ -242,16 +77,6 @@ func (n *Nocloud) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (n *Nocloud) Hostname(ctx context.Context) (hostname []byte, err error) { - return nil, nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (n *Nocloud) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - return addrs, nil -} - // KernelArgs implements the runtime.Platform interface. func (n *Nocloud) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ @@ -259,219 +84,53 @@ func (n *Nocloud) KernelArgs() procfs.Parameters { } } +// NetworkConfiguration implements the runtime.Platform interface. +// //nolint:gocyclo -func (n *Nocloud) applyNetworkConfigV1(networkConfig NetworkConfig, machineConfig *v1alpha1.Config) { - for _, network := range networkConfig.Config { - switch network.Type { - case "nameserver": - if machineConfig.MachineConfig.MachineNetwork.NameServers == nil { - machineConfig.MachineConfig.MachineNetwork.NameServers = network.Address - } - case "physical": - iface := v1alpha1.Device{ - DeviceInterface: network.Interfaces, - DeviceDHCP: false, - } +func (n *Nocloud) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + metadataConfigDl, metadataNetworkConfigDl, _, hostname, err := n.acquireConfig(ctx) + if stderrors.Is(err, errors.ErrNoConfigSource) { + err = nil + } - for _, subnet := range network.Subnets { - switch subnet.Type { - case "dhcp", "dhcp4": - iface.DeviceDHCP = true - case "static": - cidr := strings.SplitN(subnet.Address, "/", 2) - if len(cidr) == 2 { - iface.DeviceAddresses = append(iface.DeviceAddresses, - subnet.Address, - ) - } else { - mask, _ := net.IPMask(net.ParseIP(subnet.Netmask).To4()).Size() + if err != nil { + return err + } - iface.DeviceAddresses = append(iface.DeviceAddresses, - fmt.Sprintf("%s/%d", subnet.Address, mask), - ) - } + if metadataConfigDl == nil && metadataNetworkConfigDl == nil && hostname == "" { + // no data, use cached network configuration if available + return nil + } - if subnet.Gateway != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "0.0.0.0/0", - RouteGateway: subnet.Gateway, - RouteMetric: 1024, - }) - } - case "static6": - iface.DeviceAddresses = append(iface.DeviceAddresses, - subnet.Address, - ) - if subnet.Gateway != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "::/0", - RouteGateway: subnet.Gateway, - RouteMetric: 1024, - }) - } - case "ipv6_dhcpv6-stateful": - iface.DeviceDHCPOptions = &v1alpha1.DHCPOptions{ - DHCPIPv6: pointer.ToBool(true), - DHCPRouteMetric: 1024, - } - } - } + var ( + unmarshalledMetadataConfig MetadataConfig + unmarshalledNetworkConfig NetworkConfig + ) - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append( - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, - &iface, - ) + if metadataConfigDl != nil { + _ = yaml.Unmarshal(metadataConfigDl, &unmarshalledMetadataConfig) //nolint:errcheck + } + + if metadataNetworkConfigDl != nil { + if err = yaml.Unmarshal(metadataNetworkConfigDl, &unmarshalledNetworkConfig); err != nil { + return err } } -} - -//nolint:gocyclo -func (n *Nocloud) applyNetworkConfigV2(networkConfig NetworkConfig, machineConfig *v1alpha1.Config) { - var ns []string - - for name, eth := range networkConfig.Ethernets { - if !strings.HasPrefix(name, "eth") { - continue - } - - iface := v1alpha1.Device{ - DeviceInterface: name, - DeviceDHCP: eth.DHCPv4, - } - - if eth.DHCPv6 { - iface.DeviceDHCPOptions = &v1alpha1.DHCPOptions{ - DHCPIPv6: pointer.ToBool(true), - DHCPRouteMetric: 1024, - } - } - - if eth.Address != nil { - iface.DeviceAddresses = append(iface.DeviceAddresses, eth.Address...) - } - - if eth.Gateway4 != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "0.0.0.0/0", - RouteGateway: eth.Gateway4, - RouteMetric: 1024, - }) - } - - if eth.Gateway6 != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "::/0", - RouteGateway: eth.Gateway6, - RouteMetric: 1024, - }) - } - - if eth.MTU != 0 { - iface.DeviceMTU = eth.MTU - } - - if eth.NameServers.Address != nil { - ns = append(ns, eth.NameServers.Address...) - } - - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append( - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, - &iface, - ) - } - - if machineConfig.MachineConfig.MachineNetwork.NameServers == nil && ns != nil { - machineConfig.MachineConfig.MachineNetwork.NameServers = ns - } -} - -func (n *Nocloud) configFromNetwork(ctx context.Context, metaBaseURL string) (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { - log.Printf("fetching meta config from: %q", metaBaseURL+configMetaDataPath) - - metaConfig, err = download.Download(ctx, metaBaseURL+configMetaDataPath) - if err != nil { - metaConfig = nil - } - - log.Printf("fetching network config from: %q", metaBaseURL+configNetworkConfigPath) - - networkConfig, err = download.Download(ctx, metaBaseURL+configNetworkConfigPath) - if err != nil { - networkConfig = nil - } - - log.Printf("fetching machine config from: %q", metaBaseURL+configUserDataPath) - - machineConfig, err = download.Download(ctx, metaBaseURL+configUserDataPath, - download.WithErrorOnNotFound(errors.ErrNoConfigSource), - download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, nil, nil, err - } - - return metaConfig, networkConfig, machineConfig, nil -} - -//nolint:gocyclo -func (n *Nocloud) configFromCD() (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { - var dev *probe.ProbedBlockDevice - - dev, err = probe.GetDevWithFileSystemLabel(strings.ToLower(configISOLabel)) - if err != nil { - dev, err = probe.GetDevWithFileSystemLabel(strings.ToUpper(configISOLabel)) - if err != nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - } - - //nolint:errcheck - defer dev.Close() - - sb, err := filesystem.Probe(dev.Path) - if err != nil || sb == nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - log.Printf("found config disk (cidata) at %s", dev.Path) - - if err = unix.Mount(dev.Path, mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - log.Printf("fetching meta config from: cidata/%s", configMetaDataPath) - - metaConfig, err = ioutil.ReadFile(filepath.Join(mnt, configMetaDataPath)) - if err != nil { - log.Printf("failed to read %s", configMetaDataPath) - - metaConfig = nil - } - - log.Printf("fetching network config from: cidata/%s", configNetworkConfigPath) - - networkConfig, err = ioutil.ReadFile(filepath.Join(mnt, configNetworkConfigPath)) - if err != nil { - log.Printf("failed to read %s", configNetworkConfigPath) - - networkConfig = nil - } - - log.Printf("fetching machine config from: cidata/%s", configUserDataPath) - - machineConfig, err = ioutil.ReadFile(filepath.Join(mnt, configUserDataPath)) - if err != nil { - log.Printf("failed to read %s", configUserDataPath) - - machineConfig = nil - } - - if err = unix.Unmount(mnt, 0); err != nil { - return nil, nil, nil, fmt.Errorf("failed to unmount: %w", err) - } - - if machineConfig == nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - return metaConfig, networkConfig, machineConfig, nil + + if hostname == "" { + hostname = unmarshalledMetadataConfig.Hostname + } + + networkConfig, err := n.ParseMetadata(&unmarshalledNetworkConfig, hostname) + if err != nil { + return err + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go index f3a2e230d..41695afe1 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/nocloud_test.go @@ -5,131 +5,66 @@ package nocloud_test import ( + _ "embed" + "fmt" "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} +//go:embed testdata/metadata-v1.yaml +var rawMetadataV1 []byte -func (suite *ConfigSuite) TestNetworkConfigV1() { - cfg := []byte(` -version: 1 -config: - - type: physical - name: eth0 - mac_address: 'ae:71:9e:61:d0:ad' - subnets: - - type: static - address: '192.168.1.11' - netmask: '255.255.255.0' - gateway: '192.168.1.1' - - type: static6 - address: '2001:2:3:4:5:6:7:8/64' - gateway: 'fe80::1' - - type: nameserver - address: - - '192.168.1.1' - search: - - 'lan' -`) - p := &nocloud.Nocloud{} +//go:embed testdata/metadata-v2.yaml +var rawMetadataV2 []byte - defaultMachineConfig := &v1alpha1.Config{} +//go:embed testdata/expected-v1.yaml +var expectedNetworkConfigV1 string - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkHostname: "talos", - NameServers: []string{"192.168.1.1"}, - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: false, - DeviceAddresses: []string{"192.168.1.11/24", "2001:2:3:4:5:6:7:8/64"}, - DeviceRoutes: []*v1alpha1.Route{ - { - RouteNetwork: "0.0.0.0/0", - RouteGateway: "192.168.1.1", - RouteMetric: 1024, - }, - { - RouteNetwork: "::/0", - RouteGateway: "fe80::1", - RouteMetric: 1024, - }, - }, - }, - }, - }, +//go:embed testdata/expected-v2.yaml +var expectedNetworkConfigV2 string + +func TestParseMetadata(t *testing.T) { + for _, tt := range []struct { + name string + raw []byte + hostname string + expected string + }{ + { + name: "V1", + raw: rawMetadataV1, + hostname: "talos", + expected: expectedNetworkConfigV1, }, - } - - result, err := p.ConfigurationNetwork(cfg, []byte("hostname: talos"), defaultMachineConfig) - - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -// Network configs-v2 examples https://github.com/canonical/netplan/tree/main/examples - -func (suite *ConfigSuite) TestNetworkConfigV2() { - cfg := []byte(` -version: 2 -ethernets: - eth0: - dhcp4: true - addresses: - - 192.168.14.2/24 - - 2001:1::1/64 - gateway4: 192.168.14.1 - gateway6: 2001:1::2 - nameservers: - search: [foo.local, bar.local] - addresses: [8.8.8.8] -`) - p := &nocloud.Nocloud{} - - defaultMachineConfig := &v1alpha1.Config{} - - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NameServers: []string{"8.8.8.8"}, - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: true, - DeviceAddresses: []string{"192.168.14.2/24", "2001:1::1/64"}, - DeviceRoutes: []*v1alpha1.Route{ - { - RouteNetwork: "0.0.0.0/0", - RouteGateway: "192.168.14.1", - RouteMetric: 1024, - }, - { - RouteNetwork: "::/0", - RouteGateway: "2001:1::2", - RouteMetric: 1024, - }, - }, - }, - }, - }, + { + name: "V2", + raw: rawMetadataV2, + expected: expectedNetworkConfigV2, }, + } { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + n := &nocloud.Nocloud{} + + var m nocloud.NetworkConfig + + require.NoError(t, yaml.Unmarshal(tt.raw, &m)) + + networkConfig, err := n.ParseMetadata(&m, tt.hostname) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + fmt.Print(string(marshaled)) + + assert.Equal(t, tt.expected, string(marshaled)) + }) } - - result, err := p.ConfigurationNetwork(cfg, []byte{}, defaultMachineConfig) - - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml new file mode 100644 index 000000000..1dd781201 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v1.yaml @@ -0,0 +1,57 @@ +addresses: + - address: 192.168.1.0/24 + linkName: eth0 + family: inet4 + scope: global + flags: permanent + layer: platform + - address: 2001:2:3:4:5:6:7:8/64 + linkName: eth0 + family: inet6 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +routes: + - family: inet4 + dst: "" + src: "" + gateway: 192.168.1.1 + outLinkName: eth0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform + - family: inet6 + dst: "" + src: "" + gateway: fe80::1 + outLinkName: eth0 + table: main + priority: 2048 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: + - dnsServers: + - 192.168.1.1 + layer: platform +timeServers: [] +operators: [] +externalIPs: [] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml new file mode 100644 index 000000000..568ca1b57 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/expected-v2.yaml @@ -0,0 +1,60 @@ +addresses: + - address: 192.168.14.2/24 + linkName: eth0 + family: inet4 + scope: global + flags: permanent + layer: platform + - address: 2001:1::1/64 + linkName: eth0 + family: inet6 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +routes: + - family: inet4 + dst: "" + src: "" + gateway: 192.168.14.1 + outLinkName: eth0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform + - family: inet6 + dst: "" + src: "" + gateway: 2001:1::2 + outLinkName: eth0 + table: main + priority: 2048 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: [] +resolvers: + - dnsServers: + - 8.8.8.8 + layer: platform +timeServers: [] +operators: + - operator: dhcp4 + linkName: eth0 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: [] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v1.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v1.yaml new file mode 100644 index 000000000..be767300b --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v1.yaml @@ -0,0 +1,18 @@ +version: 1 +config: + - type: physical + name: eth0 + mac_address: 'ae:71:9e:61:d0:ad' + subnets: + - type: static + address: '192.168.1.11' + netmask: '255.255.255.0' + gateway: '192.168.1.1' + - type: static6 + address: '2001:2:3:4:5:6:7:8/64' + gateway: 'fe80::1' + - type: nameserver + address: + - '192.168.1.1' + search: + - 'lan' diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml new file mode 100644 index 000000000..0467d3af5 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud/testdata/metadata-v2.yaml @@ -0,0 +1,12 @@ +version: 2 +ethernets: + eth0: + dhcp4: true + addresses: + - 192.168.14.2/24 + - 2001:1::1/64 + gateway4: 192.168.14.1 + gateway6: 2001:1::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/expected.yaml new file mode 100644 index 000000000..9869635c8 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/expected.yaml @@ -0,0 +1,90 @@ +addresses: + - address: 2000:0:100::/56 + linkName: eth0 + family: inet6 + scope: global + flags: permanent + layer: platform + - address: 2000:0:ff00::/56 + linkName: eth1 + family: inet6 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 1450 + kind: "" + type: netrom + layer: platform + - name: eth1 + logical: false + up: true + mtu: 9000 + kind: "" + type: netrom + layer: platform +routes: + - family: inet6 + dst: "" + src: "" + gateway: 2000:0:100:2fff:ff:ff:ff:ff + outLinkName: eth0 + table: main + priority: 2048 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform + - family: inet6 + dst: 2000:0:100:2f00::/58 + src: "" + gateway: 2000:0:100:2fff:ff:ff:ff:f0 + outLinkName: eth0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform + - family: inet6 + dst: "" + src: "" + gateway: '2000:0:ff00::' + outLinkName: eth1 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: + - dnsServers: + - 8.8.8.8 + - 1.1.1.1 + layer: platform +timeServers: [] +operators: + - operator: dhcp4 + linkName: eth0 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform + - operator: dhcp4 + linkName: eth1 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: + - 1.2.3.4 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go new file mode 100644 index 000000000..bbb8f6018 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.go @@ -0,0 +1,194 @@ +// 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 openstack + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "path/filepath" + + "github.com/talos-systems/go-blockdevice/blockdevice/filesystem" + "github.com/talos-systems/go-blockdevice/blockdevice/probe" + "golang.org/x/sys/unix" + "inet.af/netaddr" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" + "github.com/talos-systems/talos/pkg/download" +) + +const ( + // mnt is folder to mount config drive. + mnt = "/mnt" + + // config-drive configs path. + configISOLabel = "config-2" + configMetadataPath = "openstack/latest/meta_data.json" + configNetworkDataPath = "openstack/latest/network_data.json" + configUserDataPath = "openstack/latest/user_data" + + // OpenstackExternalIPEndpoint is the local Openstack endpoint for the external IP. + OpenstackExternalIPEndpoint = "http://169.254.169.254/latest/meta-data/public-ipv4" + // OpenstackHostnameEndpoint is the local Openstack endpoint for the hostname. + OpenstackHostnameEndpoint = "http://169.254.169.254/latest/meta-data/hostname" + // OpenstackMetaDataEndpoint is the local Openstack endpoint for the meta config. + OpenstackMetaDataEndpoint = "http://169.254.169.254/" + configMetadataPath + // OpenstackNetworkDataEndpoint is the local Openstack endpoint for the network config. + OpenstackNetworkDataEndpoint = "http://169.254.169.254/" + configNetworkDataPath + // OpenstackUserDataEndpoint is the local Openstack endpoint for the config. + OpenstackUserDataEndpoint = "http://169.254.169.254/" + configUserDataPath +) + +// NetworkConfig holds NetworkData config. +type NetworkConfig struct { + Links []struct { + ID string `json:"id,omitempty"` + Type string `json:"type"` + Mac string `json:"ethernet_mac_address,omitempty"` + MTU int `json:"mtu,omitempty"` + } `json:"links"` + Networks []struct { + ID string `json:"id,omitempty"` + Link string `json:"link"` + Type string `json:"type"` + Address string `json:"ip_address,omitempty"` + Netmask string `json:"netmask,omitempty"` + Gateway string `json:"gateway,omitempty"` + Routes []struct { + Network string `json:"network,omitempty"` + Netmask string `json:"netmask,omitempty"` + Gateway string `json:"gateway,omitempty"` + } `json:"routes,omitempty"` + } `json:"networks"` + Services []struct { + Type string `json:"type"` + Address string `json:"address"` + } `json:"services,omitempty"` +} + +// MetadataConfig holds meta info. +type MetadataConfig struct { + Hostname string `json:"hostname,omitempty"` +} + +func (o *Openstack) configFromNetwork(ctx context.Context) (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { + log.Printf("fetching meta config from: %q", OpenstackMetaDataEndpoint) + + metaConfig, err = download.Download(ctx, OpenstackMetaDataEndpoint) + if err != nil { + metaConfig = nil + } + + log.Printf("fetching network config from: %q", OpenstackNetworkDataEndpoint) + + networkConfig, err = download.Download(ctx, OpenstackNetworkDataEndpoint) + if err != nil { + networkConfig = nil + } + + log.Printf("fetching machine config from: %q", OpenstackUserDataEndpoint) + + machineConfig, err = download.Download(ctx, OpenstackUserDataEndpoint, + download.WithErrorOnNotFound(errors.ErrNoConfigSource), + download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) + + return metaConfig, networkConfig, machineConfig, err +} + +func (o *Openstack) configFromCD() (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { + var dev *probe.ProbedBlockDevice + + dev, err = probe.GetDevWithFileSystemLabel(configISOLabel) + if err != nil { + return nil, nil, nil, errors.ErrNoConfigSource + } + + //nolint:errcheck + defer dev.Close() + + sb, err := filesystem.Probe(dev.Path) + if err != nil || sb == nil { + return nil, nil, nil, errors.ErrNoConfigSource + } + + log.Printf("found config disk (config-drive) at %s", dev.Path) + + if err = unix.Mount(dev.Path, mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil { + return nil, nil, nil, errors.ErrNoConfigSource + } + + log.Printf("fetching meta config from: config-drive/%s", configMetadataPath) + + metaConfig, err = ioutil.ReadFile(filepath.Join(mnt, configMetadataPath)) + if err != nil { + log.Printf("failed to read %s", configMetadataPath) + + metaConfig = nil + } + + log.Printf("fetching network config from: config-drive/%s", configNetworkDataPath) + + networkConfig, err = ioutil.ReadFile(filepath.Join(mnt, configNetworkDataPath)) + if err != nil { + log.Printf("failed to read %s", configNetworkDataPath) + + networkConfig = nil + } + + log.Printf("fetching machine config from: config-drive/%s", configUserDataPath) + + machineConfig, err = ioutil.ReadFile(filepath.Join(mnt, configUserDataPath)) + if err != nil { + log.Printf("failed to read %s", configUserDataPath) + + machineConfig = nil + } + + if err = unix.Unmount(mnt, 0); err != nil { + return nil, nil, nil, fmt.Errorf("failed to unmount: %w", err) + } + + if machineConfig == nil { + return metaConfig, networkConfig, machineConfig, errors.ErrNoConfigSource + } + + return metaConfig, networkConfig, machineConfig, nil +} + +func (o *Openstack) hostname(ctx context.Context) []byte { + log.Printf("fetching hostname from: %q", OpenstackHostnameEndpoint) + + hostname, err := download.Download(ctx, OpenstackHostnameEndpoint, + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + if err != nil { + // Platform cannot support this endpoint, or returns timeout. + log.Printf("failed to fetch hostname, ignored: %s", err) + + return nil + } + + return hostname +} + +func (o *Openstack) externalIPs(ctx context.Context) (addrs []netaddr.IP) { + log.Printf("fetching externalIP from: %q", OpenstackExternalIPEndpoint) + + exIP, err := download.Download(ctx, OpenstackExternalIPEndpoint, + download.WithErrorOnNotFound(errors.ErrNoExternalIPs), + download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) + if err != nil { + log.Printf("failed to fetch external IPs, ignored: %s", err) + + return nil + } + + if addr, err := netaddr.ParseIP(string(exIP)); err == nil { + addrs = append(addrs, addr) + } + + return addrs +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.json new file mode 100644 index 000000000..3d3cd7827 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/metadata.json @@ -0,0 +1,11 @@ +{ + "availability_zone": "nova", + "devices": [], + "hostname": "talos", + "keys": [], + "launch_index": 0, + "name": "talos", + "project_id": "39073b0a-1234-1234-1234-5e76a4bd64b2", + "public_keys": {}, + "uuid": "39073b0a-1234-1234-1234-5e76a4bd64b2" +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/network.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/network.json new file mode 100644 index 000000000..cb76f7c41 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/network.json @@ -0,0 +1,75 @@ +{ + "links": [ + { + "ethernet_mac_address": "A4:BF:00:10:20:30", + "id": "aae16046-6c74-4f33-acf2-a16e9ab093eb", + "type": "phy", + "mtu": 1450, + "vif_id": "7607af2d-c24d-4bfb-909e-c447b119f4e2" + }, + { + "ethernet_mac_address": "A4:BF:00:10:20:31", + "id": "aae16046-6c74-4f33-acf2-a16e9ab093ec", + "type": "ovs", + "mtu": 9000, + "vif_id": "c816df7e-7bcc-45ca-9eb2-3d3d3dca0639" + } + ], + "networks": [ + { + "id": "publicnet-ipv4", + "link": "aae16046-6c74-4f33-acf2-a16e9ab093eb", + "network_id": "66374c4d-5123-4f11-8fa9-8a6dea2b4fe7", + "type": "ipv4_dhcp" + }, + { + "routes": [ + { + "network": "2000:0:100:2f00::", + "gateway": "2000:0:100:2fff:ff:ff:ff:f0", + "netmask": "ffff:ffff:ffff:ffc0::" + } + ], + "dns_nameservers": [ + "2000:0:100::1" + ], + "gateway": "2000:0:100:2fff:ff:ff:ff:ff", + "link": "aae16046-6c74-4f33-acf2-a16e9ab093eb", + "ip_address": "2000:0:100::/56", + "network_id": "39b48637-d98a-4dfc-a05b-d61e8d88fafe", + "id": "publicnet-ipv6", + "type": "ipv6" + }, + { + "id": "privatnet-ipv4", + "link": "aae16046-6c74-4f33-acf2-a16e9ab093ec", + "network_id": "66374c4d-5123-4f11-8fa9-8a6dea2b4fe7", + "type": "ipv4_dhcp" + }, + { + "routes": [ + { + "network": "::", + "netmask": "::", + "gateway": "2000:0:ff00::" + } + ], + "id": "privatnet-ipv6", + "link": "aae16046-6c74-4f33-acf2-a16e9ab093ec", + "ip_address": "2000:0:ff00::1", + "netmask": "ffff:ffff:ffff:ff00::", + "network_id": "66374c4d-5123-4f11-8fa9-8a6dea2b4fe7", + "type": "ipv6" + } + ], + "services": [ + { + "address": "8.8.8.8", + "type": "dns" + }, + { + "address": "1.1.1.1", + "type": "dns" + } + ] +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go index d58886218..06809c17d 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack.go @@ -7,83 +7,21 @@ package openstack import ( "bytes" "context" + stderrors "errors" "fmt" - "io/ioutil" - "log" - "net" - "path/filepath" - "sort" "strconv" "strings" - "github.com/talos-systems/go-blockdevice/blockdevice/filesystem" - "github.com/talos-systems/go-blockdevice/blockdevice/probe" "github.com/talos-systems/go-procfs/procfs" - "golang.org/x/sys/unix" yaml "gopkg.in/yaml.v3" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" - "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) -const ( - // mnt is folder to mount config drive. - mnt = "/mnt" - - // config-drive configs path. - configISOLabel = "config-2" - configMetadataPath = "openstack/latest/meta_data.json" - configNetworkDataPath = "openstack/latest/network_data.json" - configUserDataPath = "openstack/latest/user_data" - - // OpenstackExternalIPEndpoint is the local Openstack endpoint for the external IP. - OpenstackExternalIPEndpoint = "http://169.254.169.254/latest/meta-data/public-ipv4" - // OpenstackHostnameEndpoint is the local Openstack endpoint for the hostname. - OpenstackHostnameEndpoint = "http://169.254.169.254/latest/meta-data/hostname" - // OpenstackMetaDataEndpoint is the local Openstack endpoint for the meta config. - OpenstackMetaDataEndpoint = "http://169.254.169.254/" + configMetadataPath - // OpenstackNetworkDataEndpoint is the local Openstack endpoint for the network config. - OpenstackNetworkDataEndpoint = "http://169.254.169.254/" + configNetworkDataPath - // OpenstackUserDataEndpoint is the local Openstack endpoint for the config. - OpenstackUserDataEndpoint = "http://169.254.169.254/" + configUserDataPath -) - -// NetworkConfig holds NetworkData config. -type NetworkConfig struct { - Links []struct { - ID string `yaml:"id,omitempty"` - Type string `yaml:"type"` - Mac string `yaml:"ethernet_mac_address,omitempty"` - MTU int `yaml:"mtu,omitempty"` - } `yaml:"links"` - Networks []struct { - ID string `yaml:"id,omitempty"` - Link string `yaml:"link"` - Type string `yaml:"type"` - Address string `yaml:"ip_address,omitempty"` - Netmask string `yaml:"netmask,omitempty"` - Gateway string `yaml:"gateway,omitempty"` - Routes []struct { - Network string `yaml:"network,omitempty"` - Netmask string `yaml:"netmask,omitempty"` - Gateway string `yaml:"gateway,omitempty"` - } `yaml:"routes,omitempty"` - } `yaml:"networks"` - Services []struct { - Type string `yaml:"type"` - Address string `yaml:"address"` - } `yaml:"services,omitempty"` -} - -// MetadataConfig holds meta info. -type MetadataConfig struct { - Hostname string `yaml:"hostname,omitempty"` -} - // Openstack is the concrete type that implements the runtime.Platform interface. type Openstack struct{} @@ -92,168 +30,228 @@ func (o *Openstack) Name() string { return "openstack" } -// ConfigurationNetwork implements the network configuration interface. +// ParseMetadata converts OpenStack metadata to platform network configuration. //nolint:gocyclo,cyclop -func (o *Openstack) ConfigurationNetwork(metadataNetworkConfig []byte, metadataConfig []byte, confProvider config.Provider) (config.Provider, error) { - var unmarshalledMetadataConfig MetadataConfig +func (o *Openstack) ParseMetadata(unmarshalledMetadataConfig *MetadataConfig, unmarshalledNetworkConfig *NetworkConfig, hostname string, extIPs []netaddr.IP) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - if err := yaml.Unmarshal(metadataConfig, &unmarshalledMetadataConfig); err != nil { - unmarshalledMetadataConfig = MetadataConfig{} + if hostname == "" { + hostname = unmarshalledMetadataConfig.Hostname } - machineConfig, ok := confProvider.Raw().(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") - } + if hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork.NetworkHostname == "" && unmarshalledMetadataConfig.Hostname != "" { - machineConfig.MachineConfig.MachineNetwork.NetworkHostname = unmarshalledMetadataConfig.Hostname - } - - if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil { - var unmarshalledNetworkConfig NetworkConfig - - if err := yaml.Unmarshal(metadataNetworkConfig, &unmarshalledNetworkConfig); err != nil { + if err := hostnameSpec.ParseFQDN(hostname); err != nil { return nil, err } - nameServers := []string{} + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } - for _, netsvc := range unmarshalledNetworkConfig.Services { - if netsvc.Type == "dns" { - nameServers = append(nameServers, netsvc.Address) + networkConfig.ExternalIPs = extIPs + + var dnsIPs []netaddr.IP + + for _, netsvc := range unmarshalledNetworkConfig.Services { + if netsvc.Type == "dns" { + if ip, err := netaddr.ParseIP(netsvc.Address); err == nil { + dnsIPs = append(dnsIPs, ip) + } else { + return nil, err } } - - if machineConfig.MachineConfig.MachineNetwork.NameServers == nil && len(nameServers) > 0 { - machineConfig.MachineConfig.MachineNetwork.NameServers = nameServers - } - - ifaces := make(map[string]*v1alpha1.Device) - - for idx, netLinks := range unmarshalledNetworkConfig.Links { - switch netLinks.Type { - case "phy", "vif", "ovs": - iface := &v1alpha1.Device{ - // We need to define name of interface by MAC - // I hope it will solve after https://github.com/talos-systems/talos/issues/4203, https://github.com/talos-systems/talos/issues/3265 - DeviceInterface: fmt.Sprintf("eth%d", idx), - DeviceMTU: netLinks.MTU, - } - ifaces[netLinks.ID] = iface - } - } - - for _, network := range unmarshalledNetworkConfig.Networks { - if network.ID == "" || ifaces[network.Link] == nil { - continue - } - - iface := ifaces[network.Link] - - switch network.Type { - case "ipv4_dhcp": - iface.DeviceDHCP = true - case "ipv4": - cidr := strings.SplitN(network.Address, "/", 2) - if len(cidr) == 1 { - mask, err := strconv.Atoi(network.Netmask) - if err != nil { - mask, _ = net.IPMask(network.Netmask).Size() - } - - iface.DeviceAddresses = append(iface.DeviceAddresses, fmt.Sprintf("%s/%d", network.Address, mask)) - } else { - iface.DeviceAddresses = append(iface.DeviceAddresses, network.Address) - } - - if network.Gateway != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "0.0.0.0/0", - RouteGateway: network.Gateway, - RouteMetric: 1024, - }) - } - case "ipv6": - cidr := strings.SplitN(network.Address, "/", 2) - if len(cidr) == 1 { - mask, err := strconv.Atoi(network.Netmask) - if err != nil { - mask, _ = net.IPMask(net.ParseIP(network.Netmask).To16()).Size() - } - - iface.DeviceAddresses = append(iface.DeviceAddresses, fmt.Sprintf("%s/%d", network.Address, mask)) - } else { - iface.DeviceAddresses = append(iface.DeviceAddresses, network.Address) - } - - if network.Gateway != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "::/0", - RouteGateway: network.Gateway, - RouteMetric: 1024, - }) - } - } - - for _, route := range network.Routes { - mask, err := strconv.Atoi(route.Netmask) - if err != nil { - gw := net.ParseIP(route.Network) - - if len(gw) == net.IPv4len { - mask, _ = net.IPMask(net.ParseIP(route.Netmask).To4()).Size() - } else { - mask, _ = net.IPMask(net.ParseIP(route.Netmask).To16()).Size() - } - } - - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: fmt.Sprintf("%s/%d", route.Network, mask), - RouteGateway: route.Gateway, - RouteMetric: 1024, - }) - } - } - - ifaceNames := make([]string, 0, len(ifaces)) - - for ifaceName := range ifaces { - ifaceNames = append(ifaceNames, ifaceName) - } - - sort.Strings(ifaceNames) - - for _, ifaceName := range ifaceNames { - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, ifaces[ifaceName]) - } - - if machineConfig.MachineConfig.MachineNetwork.NameServers == nil && len(nameServers) > 0 { - machineConfig.MachineConfig.MachineNetwork.NameServers = nameServers - } } - return machineConfig, nil + if len(dnsIPs) > 0 { + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + DNSServers: dnsIPs, + ConfigLayer: network.ConfigPlatform, + }) + } + + ifaces := make(map[string]string) + + for idx, netLinks := range unmarshalledNetworkConfig.Links { + switch netLinks.Type { + case "phy", "vif", "ovs": + // We need to define name of interface by MAC + // I hope it will solve after https://github.com/talos-systems/talos/issues/4203, https://github.com/talos-systems/talos/issues/3265 + ifaces[netLinks.ID] = fmt.Sprintf("eth%d", idx) + + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: ifaces[netLinks.ID], + Up: true, + MTU: uint32(netLinks.MTU), + ConfigLayer: network.ConfigPlatform, + }) + } + } + + for _, ntwrk := range unmarshalledNetworkConfig.Networks { + if ntwrk.ID == "" || ifaces[ntwrk.Link] == "" { + continue + } + + iface := ifaces[ntwrk.Link] + + switch ntwrk.Type { + case "ipv4_dhcp": + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: iface, + RequireUp: true, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + case "ipv4", "ipv6": + var ipPrefix netaddr.IPPrefix + + cidr := strings.SplitN(ntwrk.Address, "/", 2) + if len(cidr) == 1 { + ip, err := netaddr.ParseIP(ntwrk.Address) + if err != nil { + return nil, err + } + + bits, err := strconv.Atoi(ntwrk.Netmask) + if err != nil { + maskIP, err := netaddr.ParseIP(ntwrk.Netmask) + if err != nil { + return nil, err + } + + mask, _ := maskIP.MarshalBinary() //nolint:errcheck // never fails + + ipPrefix, err = ip.Netmask(mask) + if err != nil { + return nil, err + } + } else { + ipPrefix = netaddr.IPPrefixFrom(ip, uint8(bits)) + } + } else { + var err error + + ipPrefix, err = netaddr.ParseIPPrefix(ntwrk.Address) + if err != nil { + return nil, err + } + } + + family := nethelpers.FamilyInet4 + if ntwrk.Type == "ipv6" { + family = nethelpers.FamilyInet6 + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: iface, + Address: ipPrefix, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }, + ) + + if ntwrk.Gateway != "" { + gw, err := netaddr.ParseIP(ntwrk.Gateway) + if err != nil { + return nil, err + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: iface, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + Priority: 1024, + } + + if family == nethelpers.FamilyInet6 { + route.Priority = 2048 + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + } + + for _, route := range ntwrk.Routes { + gw, err := netaddr.ParseIP(route.Gateway) + if err != nil { + return nil, err + } + + destIP, err := netaddr.ParseIP(route.Network) + if err != nil { + return nil, err + } + + var dest netaddr.IPPrefix + + bits, err := strconv.Atoi(route.Netmask) + if err != nil { + var maskIP netaddr.IP + + maskIP, err = netaddr.ParseIP(route.Netmask) + if err != nil { + return nil, err + } + + mask, _ := maskIP.MarshalBinary() //nolint:errcheck + + dest, err = destIP.Netmask(mask) + if err != nil { + return nil, err + } + } else { + dest, err = destIP.Prefix(uint8(bits)) + if err != nil { + return nil, err + } + } + + family := nethelpers.FamilyInet4 + if destIP.Is6() { + family = nethelpers.FamilyInet6 + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Destination: dest, + Gateway: gw, + OutLinkName: iface, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + } + + return networkConfig, nil } // Configuration implements the runtime.Platform interface. func (o *Openstack) Configuration(ctx context.Context) (machineConfig []byte, err error) { - var ( - metadataConfigDl []byte - metadataNetworkConfigDl []byte - ) - - metadataConfigDl, metadataNetworkConfigDl, machineConfig, err = o.configFromCD() + _, _, machineConfig, err = o.configFromCD() if err != nil { - metadataConfigDl, metadataNetworkConfigDl, machineConfig, err = o.configFromNetwork(ctx) + _, _, machineConfig, err = o.configFromNetwork(ctx) if err != nil { return nil, err } @@ -265,17 +263,7 @@ func (o *Openstack) Configuration(ctx context.Context) (machineConfig []byte, er return nil, errors.ErrNoConfigSource } - confProvider, err := configloader.NewFromBytes(machineConfig) - if err != nil { - return nil, err - } - - confProvider, err = o.ConfigurationNetwork(metadataNetworkConfigDl, metadataConfigDl, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() + return machineConfig, nil } // Mode implements the runtime.Platform interface. @@ -283,41 +271,6 @@ func (o *Openstack) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (o *Openstack) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", OpenstackHostnameEndpoint) - - hostname, err = download.Download(ctx, OpenstackHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - // Platform cannot support this endpoint, or return timeout. - log.Printf("failed to fetch hostname, ignored: %s", err) - - return nil, nil - } - - return hostname, nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (o *Openstack) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - log.Printf("fetching externalIP from: %q", OpenstackExternalIPEndpoint) - - exIP, err := download.Download(ctx, OpenstackExternalIPEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil { - return nil, err - } - - if addr := net.ParseIP(string(exIP)); addr != nil { - addrs = append(addrs, addr) - } - - return addrs, nil -} - // KernelArgs implements the runtime.Platform interface. func (o *Openstack) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ @@ -325,89 +278,42 @@ func (o *Openstack) KernelArgs() procfs.Parameters { } } -func (o *Openstack) configFromNetwork(ctx context.Context) (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { - log.Printf("fetching meta config from: %q", OpenstackMetaDataEndpoint) - - metaConfig, err = download.Download(ctx, OpenstackMetaDataEndpoint) +// NetworkConfiguration implements the runtime.Platform interface. +func (o *Openstack) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + metadataConfigDl, metadataNetworkConfigDl, _, err := o.configFromCD() if err != nil { - metaConfig = nil + metadataConfigDl, metadataNetworkConfigDl, _, err = o.configFromNetwork(ctx) + if stderrors.Is(err, errors.ErrNoConfigSource) { + err = nil + } + + if err != nil { + return err + } } - log.Printf("fetching network config from: %q", OpenstackNetworkDataEndpoint) + hostname := o.hostname(ctx) + extIPs := o.externalIPs(ctx) - networkConfig, err = download.Download(ctx, OpenstackNetworkDataEndpoint) + var ( + unmarshalledMetadataConfig MetadataConfig + unmarshalledNetworkConfig NetworkConfig + ) + + // ignore errors unmarshaling, empty configs work just fine as empty default + _ = yaml.Unmarshal(metadataConfigDl, &unmarshalledMetadataConfig) //nolint:errcheck + _ = yaml.Unmarshal(metadataNetworkConfigDl, &unmarshalledNetworkConfig) //nolint:errcheck + + networkConfig, err := o.ParseMetadata(&unmarshalledMetadataConfig, &unmarshalledNetworkConfig, string(hostname), extIPs) if err != nil { - networkConfig = nil + return err } - log.Printf("fetching machine config from: %q", OpenstackUserDataEndpoint) - - machineConfig, err = download.Download(ctx, OpenstackUserDataEndpoint, - download.WithErrorOnNotFound(errors.ErrNoConfigSource), - download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, nil, nil, errors.ErrNoConfigSource + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() } - return metaConfig, networkConfig, machineConfig, nil -} - -func (o *Openstack) configFromCD() (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) { - var dev *probe.ProbedBlockDevice - - dev, err = probe.GetDevWithFileSystemLabel(configISOLabel) - if err != nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - //nolint:errcheck - defer dev.Close() - - sb, err := filesystem.Probe(dev.Path) - if err != nil || sb == nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - log.Printf("found config disk (config-drive) at %s", dev.Path) - - if err = unix.Mount(dev.Path, mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - log.Printf("fetching meta config from: config-drive/%s", configMetadataPath) - - metaConfig, err = ioutil.ReadFile(filepath.Join(mnt, configMetadataPath)) - if err != nil { - log.Printf("failed to read %s", configMetadataPath) - - metaConfig = nil - } - - log.Printf("fetching network config from: config-drive/%s", configNetworkDataPath) - - networkConfig, err = ioutil.ReadFile(filepath.Join(mnt, configNetworkDataPath)) - if err != nil { - log.Printf("failed to read %s", configNetworkDataPath) - - networkConfig = nil - } - - log.Printf("fetching machine config from: config-drive/%s", configUserDataPath) - - machineConfig, err = ioutil.ReadFile(filepath.Join(mnt, configUserDataPath)) - if err != nil { - log.Printf("failed to read %s", configUserDataPath) - - machineConfig = nil - } - - if err = unix.Unmount(mnt, 0); err != nil { - return nil, nil, nil, fmt.Errorf("failed to unmount: %w", err) - } - - if machineConfig == nil { - return nil, nil, nil, errors.ErrNoConfigSource - } - - return metaConfig, networkConfig, machineConfig, nil + return nil } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go index 158fbd4aa..c342d6c8e 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack/openstack_test.go @@ -5,161 +5,43 @@ package openstack_test import ( + _ "embed" + "encoding/json" "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} - -// https://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/metadata-service-network-info.html - -func (suite *ConfigSuite) TestNetworkConfig() { - cfg := []byte(`{ -"links": [ - { - "ethernet_mac_address": "A4:BF:00:10:20:30", - "id": "aae16046-6c74-4f33-acf2-a16e9ab093eb", - "type": "phy", - "mtu": 1450, - "vif_id": "7607af2d-c24d-4bfb-909e-c447b119f4e2" - }, - { - "ethernet_mac_address": "A4:BF:00:10:20:31", - "id": "aae16046-6c74-4f33-acf2-a16e9ab093ec", - "type": "ovs", - "mtu": 9000, - "vif_id": "c816df7e-7bcc-45ca-9eb2-3d3d3dca0639" - } -], -"networks": [ - { - "id": "publicnet-ipv4", - "link": "aae16046-6c74-4f33-acf2-a16e9ab093eb", - "network_id": "66374c4d-5123-4f11-8fa9-8a6dea2b4fe7", - "type": "ipv4_dhcp" - }, - { - "routes": [ - { - "network": "2000:0:100:2f00::", - "gateway": "2000:0:100:2fff:ff:ff:ff:f0", - "netmask": "ffff:ffff:ffff:ffc0::" - } - ], - "dns_nameservers": [ - "2000:0:100::1" - ], - "gateway": "2000:0:100:2fff:ff:ff:ff:ff", - "link": "aae16046-6c74-4f33-acf2-a16e9ab093eb", - "ip_address": "2000:0:100::/56", - "network_id": "39b48637-d98a-4dfc-a05b-d61e8d88fafe", - "id": "publicnet-ipv6", - "type": "ipv6" - }, - { - "id": "privatnet-ipv4", - "link": "aae16046-6c74-4f33-acf2-a16e9ab093ec", - "network_id": "66374c4d-5123-4f11-8fa9-8a6dea2b4fe7", - "type": "ipv4_dhcp" - }, - { - "routes": [ - { - "network": "::", - "netmask": "::", - "gateway": "2000:0:ff00::" - } - ], - "id": "privatnet-ipv6", - "link": "aae16046-6c74-4f33-acf2-a16e9ab093ec", - "ip_address": "2000:0:ff00::1", - "netmask": "ffff:ffff:ffff:ff00::", - "network_id": "66374c4d-5123-4f11-8fa9-8a6dea2b4fe7", - "type": "ipv6" - }, -], -"services": [ - { - "address": "8.8.8.8", - "type": "dns" - }, - { - "address": "1.1.1.1", - "type": "dns" - } -] -}`) - - meta := []byte(`{ -"availability_zone": "nova", -"devices": [], -"hostname": "talos", -"keys": [], -"launch_index": 0, -"name": "talos", -"project_id": "39073b0a-1234-1234-1234-5e76a4bd64b2", -"public_keys": {}, -"uuid": "39073b0a-1234-1234-1234-5e76a4bd64b2" -}`) - - p := &openstack.Openstack{} - - defaultMachineConfig := &v1alpha1.Config{} - - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkHostname: "talos", - NameServers: []string{"8.8.8.8", "1.1.1.1"}, - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceMTU: 1450, - DeviceDHCP: true, - DeviceAddresses: []string{"2000:0:100::/56"}, - DeviceRoutes: []*v1alpha1.Route{ - { - RouteNetwork: "::/0", - RouteGateway: "2000:0:100:2fff:ff:ff:ff:ff", - RouteMetric: 1024, - }, - { - RouteNetwork: "2000:0:100:2f00::/58", - RouteGateway: "2000:0:100:2fff:ff:ff:ff:f0", - RouteMetric: 1024, - }, - }, - }, - { - DeviceInterface: "eth1", - DeviceMTU: 9000, - DeviceDHCP: true, - DeviceAddresses: []string{"2000:0:ff00::1/56"}, - DeviceRoutes: []*v1alpha1.Route{ - { - RouteNetwork: "::/0", - RouteGateway: "2000:0:ff00::", - RouteMetric: 1024, - }, - }, - }, - }, - }, - }, - } - - result, err := p.ConfigurationNetwork(cfg, meta, defaultMachineConfig) - - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) +//go:embed metadata.json +var rawMetadata []byte + +//go:embed network.json +var rawNetwork []byte + +//go:embed expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { + o := &openstack.Openstack{} + + var m openstack.MetadataConfig + + require.NoError(t, json.Unmarshal(rawMetadata, &m)) + + var n openstack.NetworkConfig + + require.NoError(t, json.Unmarshal(rawNetwork, &n)) + + networkConfig, err := o.ParseMetadata(&m, &n, "", []netaddr.IP{netaddr.MustParseIP("1.2.3.4")}) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go index 890928c70..18a79a354 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle.go @@ -10,17 +10,13 @@ import ( "encoding/json" "fmt" "log" - "net" - "github.com/AlekSi/pointer" "github.com/talos-systems/go-procfs/procfs" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) // Ref: https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm @@ -51,58 +47,43 @@ func (o *Oracle) Name() string { return "oracle" } -// ConfigurationNetwork implements the network configuration interface. -func (o *Oracle) ConfigurationNetwork(metadataNetworkConfig []byte, confProvider config.Provider) (config.Provider, error) { - var machineConfig *v1alpha1.Config +// ParseMetadata converts Oracle Cloud metadata into platform network configuration. +func (o *Oracle) ParseMetadata(interfaceAddresses []NetworkConfig, hostname string) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - machineConfig, ok := confProvider.(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") + if hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } + for idx, iface := range interfaceAddresses { + ipv6 := iface.Ipv6SubnetCidrBlock != "" && iface.Ipv6VirtualRouterIP != "" - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } - - var interfaceAddresses []NetworkConfig - - if err := json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil { - return nil, err - } - - if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil { - for idx, iface := range interfaceAddresses { - ipv6 := iface.Ipv6SubnetCidrBlock != "" && iface.Ipv6VirtualRouterIP != "" - - if ipv6 { - device := &v1alpha1.Device{ - DeviceInterface: fmt.Sprintf("eth%d", idx), - DeviceDHCP: true, - DeviceDHCPOptions: &v1alpha1.DHCPOptions{DHCPIPv6: pointer.ToBool(true)}, - } - - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, device) - } + if ipv6 { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP6, + LinkName: fmt.Sprintf("eth%d", idx), + RequireUp: true, + DHCP6: network.DHCP6OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) } } - return confProvider, nil + return networkConfig, nil } // Configuration implements the platform.Platform interface. func (o *Oracle) Configuration(ctx context.Context) ([]byte, error) { - log.Printf("fetching network config from %q", OracleNetworkEndpoint) - - metadataNetworkConfig, err := download.Download(ctx, OracleNetworkEndpoint, - download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"})) - if err != nil { - return nil, fmt.Errorf("failed to fetch network config from metadata service") - } - log.Printf("fetching machine config from: %q", OracleUserDataEndpoint) machineConfigDl, err := download.Download(ctx, OracleUserDataEndpoint, @@ -118,32 +99,7 @@ func (o *Oracle) Configuration(ctx context.Context) ([]byte, error) { return nil, errors.ErrNoConfigSource } - confProvider, err := configloader.NewFromBytes(machineConfig) - if err != nil { - return nil, fmt.Errorf("error parsing machine config: %w", err) - } - - confProvider, err = o.ConfigurationNetwork(metadataNetworkConfig, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() -} - -// Hostname implements the platform.Platform interface. -func (o *Oracle) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", OracleHostnameEndpoint) - - hostname, err = download.Download(ctx, OracleHostnameEndpoint, - download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"}), - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - return nil, err - } - - return hostname, nil + return machineConfig, nil } // Mode implements the platform.Platform interface. @@ -151,14 +107,46 @@ func (o *Oracle) Mode() runtime.Mode { return runtime.ModeCloud } -// ExternalIPs implements the runtime.Platform interface. -func (o *Oracle) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - return nil, nil -} - // KernelArgs implements the runtime.Platform interface. func (o *Oracle) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty1").Append("ttyS0"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +func (o *Oracle) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching network config from %q", OracleNetworkEndpoint) + + metadataNetworkConfig, err := download.Download(ctx, OracleNetworkEndpoint, + download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"})) + if err != nil { + return fmt.Errorf("failed to fetch network config from metadata service: %w", err) + } + + var interfaceAddresses []NetworkConfig + + if err = json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil { + return err + } + + log.Printf("fetching hostname from: %q", OracleHostnameEndpoint) + + hostname, _ := download.Download(ctx, OracleHostnameEndpoint, //nolint:errcheck + download.WithHeaders(map[string]string{"Authorization": "Bearer Oracle"}), + download.WithErrorOnNotFound(errors.ErrNoHostname), + download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) + + networkConfig, err := o.ParseMetadata(interfaceAddresses, string(hostname)) + if err != nil { + return err + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go index 10dd7c851..a1efd6834 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/oracle_test.go @@ -5,56 +5,35 @@ package oracle_test import ( + _ "embed" + "encoding/json" "testing" - "github.com/AlekSi/pointer" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} - -func (suite *ConfigSuite) TestNetworkConfig() { - cfg := []byte(` -[ { - "vnicId" : "ocid1.vnic.oc1.eu-amsterdam-1.asdasd", - "privateIp" : "172.16.1.11", - "vlanTag" : 1, - "macAddr" : "02:00:17:00:00:00", - "virtualRouterIp" : "172.16.1.1", - "subnetCidrBlock" : "172.16.1.0/24", - "ipv6SubnetCidrBlock" : "2603:a:b:c::/64", - "ipv6VirtualRouterIp" : "fe80::a:b:c:d" -} ] -`) - a := &oracle.Oracle{} - - defaultMachineConfig := &v1alpha1.Config{} - - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: true, - DeviceDHCPOptions: &v1alpha1.DHCPOptions{DHCPIPv6: pointer.ToBool(true)}, - }, - }, - }, - }, - } - - result, err := a.ConfigurationNetwork(cfg, defaultMachineConfig) - - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) +//go:embed testdata/metadata.json +var rawMetadata []byte + +//go:embed testdata/expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { + o := &oracle.Oracle{} + + var m []oracle.NetworkConfig + + require.NoError(t, json.Unmarshal(rawMetadata, &m)) + + networkConfig, err := o.ParseMetadata(m, "talos") + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml new file mode 100644 index 000000000..9ffa81af4 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/expected.yaml @@ -0,0 +1,17 @@ +addresses: [] +links: [] +routes: [] +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: [] +timeServers: [] +operators: + - operator: dhcp6 + linkName: eth0 + requireUp: true + dhcp6: + routeMetric: 1024 + layer: platform +externalIPs: [] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json new file mode 100644 index 000000000..4dc7fcd01 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle/testdata/metadata.json @@ -0,0 +1,12 @@ +[ + { + "vnicId": "ocid1.vnic.oc1.eu-amsterdam-1.asdasd", + "privateIp": "172.16.1.11", + "vlanTag": 1, + "macAddr": "02:00:17:00:00:00", + "virtualRouterIp": "172.16.1.1", + "subnetCidrBlock": "172.16.1.0/24", + "ipv6SubnetCidrBlock": "2603:a:b:c::/64", + "ipv6VirtualRouterIp": "fe80::a:b:c:d" + } +] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet.go index 3acb00a71..3068f538f 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet.go @@ -12,14 +12,15 @@ import ( "net" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" - "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" + networkadapter "github.com/talos-systems/talos/internal/app/machined/pkg/adapters/network" + networkctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) // Metadata holds packet metadata info. @@ -50,14 +51,15 @@ type Interface struct { // Address holds address info from the packet metadata. type Address struct { - Public bool `json:"public"` - Enabled bool `json:"enabled"` - CIDR int `json:"cidr"` - Family int `json:"address_family"` - Netmask string `json:"netmask"` - Network string `json:"network"` - Address string `json:"address"` - Gateway string `json:"gateway"` + Public bool `json:"public"` + Management bool `json:"management"` + Enabled bool `json:"enabled"` + CIDR int `json:"cidr"` + Family int `json:"address_family"` + Netmask string `json:"netmask"` + Network string `json:"network"` + Address string `json:"address"` + Gateway string `json:"gateway"` } const ( @@ -67,7 +69,7 @@ const ( PacketMetaDataEndpoint = "https://metadata.platformequinix.com/metadata" ) -// Packet is a discoverer for non-cloud environments. +// Packet is a platform for Equinix Metal cloud. type Packet struct{} // Name implements the platform.Platform interface. @@ -76,143 +78,12 @@ func (p *Packet) Name() string { } // Configuration implements the platform.Platform interface. -//nolint:gocyclo,cyclop func (p *Packet) Configuration(ctx context.Context) ([]byte, error) { - // Fetch and unmarshal both the talos machine config and the - // metadata about the instance from packet's metadata server log.Printf("fetching machine config from: %q", PacketUserDataEndpoint) - machineConfigDl, err := download.Download(ctx, PacketUserDataEndpoint, + return download.Download(ctx, PacketUserDataEndpoint, download.WithErrorOnNotFound(errors.ErrNoConfigSource), download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, err - } - - log.Printf("fetching equinix network config from: %q", PacketMetaDataEndpoint) - - metadataConfig, err := download.Download(ctx, PacketMetaDataEndpoint) - if err != nil { - return nil, err - } - - var unmarshalledMetadataConfig Metadata - if err = json.Unmarshal(metadataConfig, &unmarshalledMetadataConfig); err != nil { - return nil, err - } - - confProvider, err := configloader.NewFromBytes(machineConfigDl) - if err != nil { - return nil, err - } - - var machineConfig *v1alpha1.Config - - machineConfig, ok := confProvider.Raw().(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") - } - - // translate the int returned from bond mode metadata to the type needed by networkd - bondMode := nethelpers.BondMode(uint8(unmarshalledMetadataConfig.Network.Bonding.Mode)) - - // determine bond name and build list of interfaces enslaved by the bond - devicesInBond := []string{} - bondName := "" - - hostInterfaces, err := net.Interfaces() - if err != nil { - return nil, fmt.Errorf("error listing host interfaces: %w", err) - } - - for _, iface := range unmarshalledMetadataConfig.Network.Interfaces { - if iface.Bond == "" { - continue - } - - if bondName != "" && iface.Bond != bondName { - return nil, fmt.Errorf("encountered multiple bonds. this is unexpected in the equinix metal platform") - } - - found := false - - for _, hostIf := range hostInterfaces { - if hostIf.HardwareAddr.String() == iface.MAC { - found = true - - devicesInBond = append(devicesInBond, hostIf.Name) - - break - } - } - - if !found { - log.Printf("interface with MAC %q wasn't found on the host, skipping", iface.MAC) - - continue - } - - bondName = iface.Bond - } - - bondDev := v1alpha1.Device{ - DeviceInterface: bondName, - DeviceDHCP: false, - DeviceBond: &v1alpha1.Bond{ - BondMode: bondMode.String(), - BondDownDelay: 200, - BondMIIMon: 100, - BondUpDelay: 200, - BondHashPolicy: "layer3+4", - BondInterfaces: devicesInBond, - }, - } - - for _, addr := range unmarshalledMetadataConfig.Network.Addresses { - bondDev.DeviceAddresses = append(bondDev.DeviceAddresses, - fmt.Sprintf("%s/%d", addr.Address, addr.CIDR), - ) - - if addr.Public { - // for "Public" address add the default route - switch addr.Family { - case 4: - bondDev.DeviceRoutes = append(bondDev.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "0.0.0.0/0", - RouteGateway: addr.Gateway, - }) - case 6: - bondDev.DeviceRoutes = append(bondDev.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: "::/0", - RouteGateway: addr.Gateway, - RouteMetric: 2 * network.DefaultRouteMetric, - }) - } - } else { - // for "Private" addresses, we add a route that goes out the gateway for the private subnets. - for _, privSubnet := range unmarshalledMetadataConfig.PrivateSubnets { - bondDev.DeviceRoutes = append(bondDev.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: privSubnet, - RouteGateway: addr.Gateway, - }) - } - } - } - - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } - - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } - - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append( - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, - &bondDev, - ) - - return machineConfig.Bytes() } // Mode implements the platform.Platform interface. @@ -220,31 +91,237 @@ func (p *Packet) Mode() runtime.Mode { return runtime.ModeMetal } -// Hostname implements the platform.Platform interface. -func (p *Packet) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching equinix metadata from: %q", PacketMetaDataEndpoint) - - metadataConfig, err := download.Download(ctx, PacketMetaDataEndpoint) - if err != nil { - return nil, err - } - - var unmarshalledMetadataConfig Metadata - if err = json.Unmarshal(metadataConfig, &unmarshalledMetadataConfig); err != nil { - return nil, err - } - - return []byte(unmarshalledMetadataConfig.Hostname), nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (p *Packet) ExternalIPs(context.Context) (addrs []net.IP, err error) { - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (p *Packet) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("ttyS1,115200n8"), } } + +// ParseMetadata converts Equinix Metal (Packet) metadata into Talos network configuration. +// +//nolint:gocyclo,cyclop +func (p *Packet) ParseMetadata(packetMetadata *Metadata) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} + + // 1. Links + + // translate the int returned from bond mode metadata to the type needed by network resources + bondMode := nethelpers.BondMode(uint8(packetMetadata.Network.Bonding.Mode)) + + // determine bond name and build list of interfaces enslaved by the bond + bondName := "" + + hostInterfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("error listing host interfaces: %w", err) + } + + for _, iface := range packetMetadata.Network.Interfaces { + if iface.Bond == "" { + continue + } + + if bondName != "" && iface.Bond != bondName { + return nil, fmt.Errorf("encountered multiple bonds. this is unexpected in the equinix metal platform") + } + + bondName = iface.Bond + + found := false + + for _, hostIf := range hostInterfaces { + if hostIf.HardwareAddr.String() == iface.MAC { + found = true + + networkConfig.Links = append(networkConfig.Links, + network.LinkSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Name: hostIf.Name, + Up: true, + MasterName: bondName, + }) + + break + } + } + + if !found { + log.Printf("interface with MAC %q wasn't found on the host, adding with the name from metadata", iface.MAC) + + networkConfig.Links = append(networkConfig.Links, + network.LinkSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Name: iface.Name, + Up: true, + MasterName: bondName, + }) + } + } + + bondLink := network.LinkSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Name: bondName, + Logical: true, + Up: true, + Kind: network.LinkKindBond, + Type: nethelpers.LinkEther, + BondMaster: network.BondMasterSpec{ + Mode: bondMode, + DownDelay: 200, + MIIMon: 100, + UpDelay: 200, + HashPolicy: nethelpers.BondXmitPolicyLayer34, + }, + } + + networkadapter.BondMasterSpec(&bondLink.BondMaster).FillDefaults() + + networkConfig.Links = append(networkConfig.Links, bondLink) + + // 2. addresses + + for _, addr := range packetMetadata.Network.Addresses { + if !(addr.Enabled && addr.Management) { + continue + } + + ipAddr, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/%d", addr.Address, addr.CIDR)) + if err != nil { + return nil, err + } + + family := nethelpers.FamilyInet4 + if ipAddr.IP().Is6() { + family = nethelpers.FamilyInet6 + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: bondName, + Address: ipAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }, + ) + } + + // 3. routes + + for _, addr := range packetMetadata.Network.Addresses { + if !(addr.Enabled && addr.Management) { + continue + } + + ipAddr, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/%d", addr.Address, addr.CIDR)) + if err != nil { + return nil, err + } + + family := nethelpers.FamilyInet4 + if ipAddr.IP().Is6() { + family = nethelpers.FamilyInet6 + } + + if addr.Public { + // for "Public" address add the default route + gw, err := netaddr.ParseIP(addr.Gateway) + if err != nil { + return nil, err + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: bondName, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + Priority: networkctrl.DefaultRouteMetric, + } + + if addr.Family == 6 { + route.Priority = 2 * networkctrl.DefaultRouteMetric + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } else { + // for "Private" addresses, we add a route that goes out the gateway for the private subnets. + for _, privSubnet := range packetMetadata.PrivateSubnets { + gw, err := netaddr.ParseIP(addr.Gateway) + if err != nil { + return nil, err + } + + dest, err := netaddr.ParseIPPrefix(privSubnet) + if err != nil { + return nil, err + } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + Destination: dest, + OutLinkName: bondName, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) + } + } + } + + // 4. hostname + + if packetMetadata.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(packetMetadata.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) + } + + return networkConfig, nil +} + +// NetworkConfiguration implements the runtime.Platform interface. +func (p *Packet) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching equinix network config from: %q", PacketMetaDataEndpoint) + + metadataConfig, err := download.Download(ctx, PacketMetaDataEndpoint) + if err != nil { + return err + } + + var packetMetadata Metadata + if err = json.Unmarshal(metadataConfig, &packetMetadata); err != nil { + return err + } + + networkConfig, err := p.ParseMetadata(&packetMetadata) + if err != nil { + return err + } + + select { + case <-ctx.Done(): + return ctx.Err() + case ch <- networkConfig: + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet_test.go index 49f9ccdc7..926652581 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/packet_test.go @@ -4,11 +4,36 @@ package packet_test -import "testing" +import ( + _ "embed" + "encoding/json" + "testing" -func TestEmpty(t *testing.T) { - // added for accurate coverage estimation - // - // please remove it once any unit-test is added - // for this package + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/packet" +) + +//go:embed testdata/metadata.json +var rawMetadata []byte + +//go:embed testdata/expected.yaml +var expectedNetworkConfig string + +func TestParseMetadata(t *testing.T) { + p := &packet.Packet{} + + var m packet.Metadata + + require.NoError(t, json.Unmarshal(rawMetadata, &m)) + + networkConfig, err := p.ParseMetadata(&m) + require.NoError(t, err) + + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/testdata/expected.yaml new file mode 100644 index 000000000..1ea7a966d --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/testdata/expected.yaml @@ -0,0 +1,104 @@ +addresses: + - address: 147.75.78.41/31 + linkName: bond0 + family: inet4 + scope: global + flags: permanent + layer: platform + - address: 2604:1380:45d1:fd00::11/127 + linkName: bond0 + family: inet6 + scope: global + flags: permanent + layer: platform + - address: 10.66.142.17/31 + linkName: bond0 + family: inet4 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + masterName: bond0 + layer: platform + - name: eth1 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + masterName: bond0 + layer: platform + - name: bond0 + logical: true + up: true + mtu: 0 + kind: bond + type: ether + bondMaster: + mode: 802.3ad + xmitHashPolicy: layer3+4 + lacpRate: slow + arpValidate: none + arpAllTargets: any + primaryReselect: always + failOverMac: 0 + miimon: 100 + updelay: 200 + downdelay: 200 + resendIgmp: 1 + lpInterval: 1 + packetsPerSlave: 1 + numPeerNotif: 1 + tlbLogicalLb: 1 + adActorSysPrio: 65535 + layer: platform +routes: + - family: inet4 + dst: "" + src: "" + gateway: 147.75.78.40 + outLinkName: bond0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform + - family: inet6 + dst: "" + src: "" + gateway: 2604:1380:45d1:fd00::10 + outLinkName: bond0 + table: main + priority: 2048 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform + - family: inet4 + dst: 10.0.0.0/8 + src: "" + gateway: 10.66.142.16 + outLinkName: bond0 + table: main + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: + - hostname: infra-green-ci + domainname: "" + layer: platform +resolvers: [] +timeServers: [] +operators: [] +externalIPs: [] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/testdata/metadata.json new file mode 100644 index 000000000..84fec3771 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/packet/testdata/metadata.json @@ -0,0 +1,117 @@ +{ + "id": "X", + "hostname": "infra-green-ci", + "plan": "c3.medium.x86", + "reserved": false, + "class": "c3.medium.x86", + "facility": "ny5", + "metro": "ny", + "private_subnets": [ + "10.0.0.0/8" + ], + "tags": [], + "ssh_keys": [ + ], + "customdata": {}, + "network": { + "bonding": { + "mode": 4, + "link_aggregation": "mlag_ha", + "mac": "68:05:ca:b8:f1:f8" + }, + "interfaces": [ + { + "name": "eth0", + "mac": "68:05:ca:b8:f1:f8", + "bond": "bond0" + }, + { + "name": "eth1", + "mac": "68:05:ca:b8:f1:f9", + "bond": "bond0" + } + ], + "addresses": [ + { + "id": "d6be5d63-50f8-452c-b5cd-6cba42fbd5b3", + "address_family": 4, + "netmask": "255.255.255.254", + "created_at": "2021-11-24T20:24:54Z", + "public": true, + "cidr": 31, + "management": true, + "enabled": true, + "network": "147.75.78.40", + "address": "147.75.78.41", + "gateway": "147.75.78.40", + "parent_block": { + "network": "147.75.78.40", + "netmask": "255.255.255.254", + "cidr": 31, + "href": "/ips/e5cc5a4e-1d80-42c8-b5ea-39644effb407" + } + }, + { + "id": "09c743e9-52e4-4125-9e88-da56c8e62ae4", + "address_family": 6, + "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", + "created_at": "2021-11-24T20:24:54Z", + "public": true, + "cidr": 127, + "management": true, + "enabled": true, + "network": "2604:1380:45d1:fd00::10", + "address": "2604:1380:45d1:fd00::11", + "gateway": "2604:1380:45d1:fd00::10", + "parent_block": { + "network": "2604:1380:45d1:fd00:0000:0000:0000:0000", + "netmask": "ffff:ffff:ffff:ff00:0000:0000:0000:0000", + "cidr": 56, + "href": "/ips/a76e6dd1-a22a-4f8a-a04d-7b68b4f358e5" + } + }, + { + "id": "c7d3cd31-beae-460a-b008-29776c95562b", + "address_family": 4, + "netmask": "255.255.255.255", + "created_at": "2021-12-10T13:41:14Z", + "public": true, + "cidr": 32, + "management": false, + "enabled": true, + "network": "147.75.195.143", + "address": "147.75.195.143", + "gateway": "147.75.195.143", + "parent_block": { + "network": "147.75.195.143", + "netmask": "255.255.255.255", + "cidr": 32, + "href": "/ips/77e054ac-cd40-473a-9c59-8f6c322c5a20" + } + }, + { + "id": "5e0bc796-9e7f-46a5-9472-ced35b8acb6d", + "address_family": 4, + "netmask": "255.255.255.254", + "created_at": "2021-11-24T20:24:54Z", + "public": false, + "cidr": 31, + "management": true, + "enabled": true, + "network": "10.66.142.16", + "address": "10.66.142.17", + "gateway": "10.66.142.16", + "parent_block": { + "network": "10.66.142.0", + "netmask": "255.255.255.128", + "cidr": 25, + "href": "/ips/045b5dd5-6a32-48e6-870d-8ea9a39169d6" + } + } + ], + "metal_gateways": [] + }, + "api_url": "https://metadata.packet.net", + "phone_home_url": "http://tinkerbell.ny5.packet.net/phone-home", + "user_state_url": "http://tinkerbell.ny5.packet.net/events" +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go index 5b4eca97c..8d44d8485 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway.go @@ -7,19 +7,18 @@ package scaleway import ( "context" "encoding/json" - "fmt" "log" - "net" + "strconv" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const ( @@ -35,66 +34,97 @@ func (s *Scaleway) Name() string { return "scaleway" } -// ConfigurationNetwork implements the network configuration interface. -func (s *Scaleway) ConfigurationNetwork(metadataConfig *instance.Metadata, confProvider config.Provider) (config.Provider, error) { - var machineConfig *v1alpha1.Config +// ParseMetadata converts Scaleway met. +func (s *Scaleway) ParseMetadata(metadataConfig *instance.Metadata) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - machineConfig, ok := confProvider.(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") + if metadataConfig.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(metadataConfig.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} + if metadataConfig.PublicIP.Address != "" { + ip, err := netaddr.ParseIP(metadataConfig.PublicIP.Address) + if err != nil { + return nil, err + } + + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) } - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: "eth0", + Up: true, + ConfigLayer: network.ConfigPlatform, + }) - iface := v1alpha1.Device{ - DeviceInterface: "eth0", - DeviceDHCP: true, - } + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: "eth0", + RequireUp: true, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) if metadataConfig.IPv6.Address != "" { - iface.DeviceAddresses = append(iface.DeviceAddresses, - fmt.Sprintf("%s/%s", metadataConfig.IPv6.Address, metadataConfig.IPv6.Netmask), + bits, err := strconv.Atoi(metadataConfig.IPv6.Netmask) + if err != nil { + return nil, err + } + + ip, err := netaddr.ParseIP(metadataConfig.IPv6.Address) + if err != nil { + return nil, err + } + + addr := netaddr.IPPrefixFrom(ip, uint8(bits)) + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: "eth0", + Address: addr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: nethelpers.FamilyInet6, + }, ) - iface.DeviceRoutes = []*v1alpha1.Route{ - { - RouteNetwork: "::/0", - RouteGateway: metadataConfig.IPv6.Gateway, - RouteMetric: 1024, - }, + gw, err := netaddr.ParseIP(metadataConfig.IPv6.Gateway) + if err != nil { + return nil, err } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + OutLinkName: "eth0", + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: nethelpers.FamilyInet6, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) } - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append( - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, - &iface, - ) - - return confProvider, nil + return networkConfig, nil } // Configuration implements the runtime.Platform interface. func (s *Scaleway) Configuration(ctx context.Context) ([]byte, error) { - log.Printf("fetching scaleway instance config from: %q ", ScalewayMetadataEndpoint) - - metadataDl, err := download.Download(ctx, ScalewayMetadataEndpoint, - download.WithErrorOnNotFound(errors.ErrNoConfigSource), - download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, errors.ErrNoConfigSource - } - - metadata := &instance.Metadata{} - if err = json.Unmarshal(metadataDl, metadata); err != nil { - return nil, errors.ErrNoConfigSource - } - log.Printf("fetching machine config from scaleway metadata server") instanceAPI := instance.NewMetadataAPI() @@ -104,17 +134,7 @@ func (s *Scaleway) Configuration(ctx context.Context) ([]byte, error) { return nil, errors.ErrNoConfigSource } - confProvider, err := configloader.NewFromBytes(machineConfigDl) - if err != nil { - return nil, err - } - - confProvider, err = s.ConfigurationNetwork(metadata, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() + return machineConfigDl, nil } // Mode implements the runtime.Platform interface. @@ -122,49 +142,26 @@ func (s *Scaleway) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (s *Scaleway) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", ScalewayMetadataEndpoint) - - metadataDl, err := download.Download(ctx, ScalewayMetadataEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - return nil, err - } - - metadata := &instance.Metadata{} - if err = json.Unmarshal(metadataDl, metadata); err != nil { - return nil, err - } - - return []byte(metadata.Hostname), nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (s *Scaleway) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - log.Printf("fetching external IP from: %q", ScalewayMetadataEndpoint) - - metadataDl, err := download.Download(ctx, ScalewayMetadataEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil { - return addrs, err - } - - metadata := &instance.Metadata{} - if err = json.Unmarshal(metadataDl, metadata); err != nil { - return addrs, err - } - - addrs = append(addrs, net.ParseIP(metadata.PublicIP.Address)) - - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (s *Scaleway) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty1").Append("ttyS0"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +func (s *Scaleway) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching scaleway instance config from: %q ", ScalewayMetadataEndpoint) + + metadataDl, err := download.Download(ctx, ScalewayMetadataEndpoint) + if err != nil { + return err + } + + metadata := &instance.Metadata{} + if err = json.Unmarshal(metadataDl, metadata); err != nil { + return err + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go index a4d0853f5..780b54b63 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/scaleway_test.go @@ -5,76 +5,36 @@ package scaleway_test import ( + _ "embed" "encoding/json" "testing" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} +//go:embed testdata/metadata.json +var rawMetadata []byte -func (suite *ConfigSuite) TestNetworkConfig() { - cfg := []byte(`{ -"id": "11111111-1111-1111-1111-111111111111", -"name": "scw-talos", -"commercial_type": "DEV1-S", -"hostname": "scw-talos", -"tags": [], -"state_detail": "booted", -"public_ip": { - "id": "11111111-1111-1111-1111-111111111111", - "address": "11.22.222.222", - "dynamic": false -}, -"private_ip": "10.00.222.222", -"ipv6": { - "address": "2001:111:222:3333::1", - "gateway": "2001:111:222:3333::", - "netmask": "64" -} -}`) - - metadata := &instance.Metadata{} - err := json.Unmarshal(cfg, &metadata) - suite.Require().NoError(err) +//go:embed testdata/expected.yaml +var expectedNetworkConfig string +func TestParseMetadata(t *testing.T) { p := &scaleway.Scaleway{} - defaultMachineConfig := &v1alpha1.Config{} + var m instance.Metadata - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: true, - DeviceAddresses: []string{"2001:111:222:3333::1/64"}, - DeviceRoutes: []*v1alpha1.Route{ - { - RouteNetwork: "::/0", - RouteGateway: "2001:111:222:3333::", - RouteMetric: 1024, - }, - }, - }, - }, - }, - }, - } + require.NoError(t, json.Unmarshal(rawMetadata, &m)) - result, err := p.ConfigurationNetwork(metadata, defaultMachineConfig) + networkConfig, err := p.ParseMetadata(&m) + require.NoError(t, err) - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml new file mode 100644 index 000000000..773f7733f --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/expected.yaml @@ -0,0 +1,43 @@ +addresses: + - address: 2001:111:222:3333::1/64 + linkName: eth0 + family: inet6 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +routes: + - family: inet6 + dst: "" + src: "" + gateway: '2001:111:222:3333::' + outLinkName: eth0 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: + - hostname: scw-talos + domainname: "" + layer: platform +resolvers: [] +timeServers: [] +operators: + - operator: dhcp4 + linkName: eth0 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: + - 11.22.222.222 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/metadata.json new file mode 100644 index 000000000..bbe8e2b29 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway/testdata/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "11111111-1111-1111-1111-111111111111", + "name": "scw-talos", + "commercial_type": "DEV1-S", + "hostname": "scw-talos", + "tags": [], + "state_detail": "booted", + "public_ip": { + "id": "11111111-1111-1111-1111-111111111111", + "address": "11.22.222.222", + "dynamic": false + }, + "private_ip": "10.00.222.222", + "ipv6": { + "address": "2001:111:222:3333::1", + "gateway": "2001:111:222:3333::", + "netmask": "64" + } +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml new file mode 100644 index 000000000..4c5e2838a --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/expected.yaml @@ -0,0 +1,75 @@ +addresses: + - address: 185.70.197.3/32 + linkName: eth0 + family: inet4 + scope: global + flags: permanent + layer: platform + - address: 2a04:3544:8000:1000:0:1111:2222:3333/64 + linkName: eth2 + family: inet6 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform + - name: eth1 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform + - name: eth2 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform +routes: + - family: inet6 + dst: 2a04:3544:8000:1000::/64 + src: "" + gateway: 2a04:3544:8000:1000::1 + outLinkName: eth2 + table: main + priority: 1024 + scope: global + type: unicast + flags: "" + protocol: static + layer: platform +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: + - dnsServers: + - 94.237.127.9 + - 94.237.40.9 + - 2a04:3540:53::1 + - 2a04:3544:53::1 + layer: platform +timeServers: [] +operators: + - operator: dhcp4 + linkName: eth0 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform + - operator: dhcp4 + linkName: eth1 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: + - 185.70.197.2 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/metadata.json new file mode 100644 index 000000000..688be61cb --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/testdata/metadata.json @@ -0,0 +1,83 @@ +{ + "cloud_name": "upcloud", + "instance_id": "00123456-1111-2222-3333-123456789012", + "hostname": "talos", + "network": { + "interfaces": [ + { + "index": 1, + "ip_addresses": [ + { + "address": "185.70.197.2", + "dhcp": true, + "dns": [ + "94.237.127.9", + "94.237.40.9" + ], + "family": "IPv4", + "floating": false, + "gateway": "185.70.196.1", + "network": "185.70.196.0/22" + }, + { + "address": "185.70.197.3", + "dhcp": false, + "dns": null, + "family": "IPv4", + "floating": true, + "gateway": "", + "network": "185.70.197.3/32" + } + ], + "mac": "5e:bf:5e:02:28:07", + "network_id": "035ef879-1111-2222-3333-123456789012", + "type": "public" + }, + { + "index": 2, + "ip_addresses": [ + { + "address": "10.11.0.2", + "dhcp": true, + "dns": null, + "family": "IPv4", + "floating": false, + "gateway": "10.11.0.1", + "network": "10.11.0.0/22" + } + ], + "mac": "5e:bf:5e:02:cd:e0", + "network_id": "031c9f9c-1111-2222-3333-123456789012", + "type": "utility" + }, + { + "index": 3, + "ip_addresses": [ + { + "address": "2a04:3544:8000:1000:0000:1111:2222:3333", + "dhcp": false, + "dns": [ + "2a04:3540:53::1", + "2a04:3544:53::1" + ], + "family": "IPv6", + "floating": false, + "gateway": "2a04:3544:8000:1000::1", + "network": "2a04:3544:8000:1000::/64" + } + ], + "mac": "5e:bf:5e:02:78:a4", + "network_id": "03b326a2-1111-2222-3333-123456789012", + "type": "public" + } + ], + "dns": [ + "94.237.127.9", + "94.237.40.9" + ] + }, + "storage": {}, + "tags": [], + "user_data": "", + "vendor_data": "" +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go index 31c369cc5..396f06225 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud.go @@ -9,28 +9,21 @@ import ( "encoding/json" "fmt" "log" - "net" "github.com/talos-systems/go-procfs/procfs" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const ( // UpCloudMetadataEndpoint is the local UpCloud endpoint. UpCloudMetadataEndpoint = "http://169.254.169.254/metadata/v1.json" - // UpCloudExternalIPEndpoint is the local UpCloud endpoint for the external IP. - UpCloudExternalIPEndpoint = "http://169.254.169.254/metadata/v1/network/interfaces/1/ip_addresses/1/address" - - // UpCloudHostnameEndpoint is the local UpCloud endpoint for the hostname. - UpCloudHostnameEndpoint = "http://169.254.169.254/metadata/v1/hostname" - // UpCloudUserDataEndpoint is the local UpCloud endpoint for the config. UpCloudUserDataEndpoint = "http://169.254.169.254/metadata/v1/user_data" ) @@ -70,97 +63,143 @@ func (u *UpCloud) Name() string { return "upcloud" } -// ConfigurationNetwork implements the network configuration interface. +// ParseMetadata converts Upcloud metadata into platform network configuration. +// //nolint:gocyclo -func (u *UpCloud) ConfigurationNetwork(metadataConfig []byte, confProvider config.Provider) (config.Provider, error) { - var machineConfig *v1alpha1.Config +func (u *UpCloud) ParseMetadata(meta *MetaData) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - machineConfig, ok := confProvider.Raw().(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") + if meta.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(meta.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - meta := &MetaData{} - if err := json.Unmarshal(metadataConfig, meta); err != nil { - return nil, err - } + var dnsIPs []netaddr.IP - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } + firstIP := true - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } + for _, addr := range meta.Network.Interfaces { + if addr.Index <= 0 { // protect from negative interface name + continue + } - if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil { - for _, addr := range meta.Network.Interfaces { - if addr.Index <= 0 { // protect from negative interface name - continue - } + iface := fmt.Sprintf("eth%d", addr.Index-1) - iface := &v1alpha1.Device{ - DeviceInterface: fmt.Sprintf("eth%d", addr.Index-1), - } + networkConfig.Links = append(networkConfig.Links, network.LinkSpecSpec{ + Name: iface, + Up: true, + ConfigLayer: network.ConfigPlatform, + }) - for _, ip := range addr.IPAddresses { - if ip.DHCP && ip.Family == "IPv4" { - iface.DeviceDHCP = true + for _, ip := range addr.IPAddresses { + if firstIP { + ipAddr, err := netaddr.ParseIP(ip.Address) + if err != nil { + return nil, err } - if !ip.DHCP { - if ip.Floating { - iface.DeviceAddresses = append(iface.DeviceAddresses, ip.Network) - } else { - iface.DeviceAddresses = append(iface.DeviceAddresses, ip.Address) + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ipAddr) - if ip.Gateway != "" { - iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{ - RouteNetwork: ip.Network, - RouteGateway: ip.Gateway, - RouteMetric: 1024, - }) - } + firstIP = false + } + + for _, addr := range ip.DNS { + if ipAddr, err := netaddr.ParseIP(addr); err == nil { + dnsIPs = append(dnsIPs, ipAddr) + } + } + + if ip.DHCP && ip.Family == "IPv4" { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: iface, + RequireUp: true, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + } + + if !ip.DHCP { + ntwrk, err := netaddr.ParseIPPrefix(ip.Network) + if err != nil { + return nil, err + } + + addr, err := netaddr.ParseIP(ip.Address) + if err != nil { + return nil, err + } + + ipPrefix := netaddr.IPPrefixFrom(addr, ntwrk.Bits()) + + family := nethelpers.FamilyInet4 + if addr.Is6() { + family = nethelpers.FamilyInet6 + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: iface, + Address: ipPrefix, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: family, + }, + ) + + if ip.Gateway != "" { + gw, err := netaddr.ParseIP(ip.Gateway) + if err != nil { + return nil, err } + + route := network.RouteSpecSpec{ + ConfigLayer: network.ConfigPlatform, + Gateway: gw, + Destination: ntwrk, + OutLinkName: iface, + Table: nethelpers.TableMain, + Protocol: nethelpers.ProtocolStatic, + Type: nethelpers.TypeUnicast, + Family: family, + Priority: 1024, + } + + route.Normalize() + + networkConfig.Routes = append(networkConfig.Routes, route) } } - - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, iface) } } - return machineConfig, nil + if len(dnsIPs) > 0 { + networkConfig.Resolvers = append(networkConfig.Resolvers, network.ResolverSpecSpec{ + DNSServers: dnsIPs, + ConfigLayer: network.ConfigPlatform, + }) + } + + return networkConfig, nil } // Configuration implements the runtime.Platform interface. func (u *UpCloud) Configuration(ctx context.Context) ([]byte, error) { - log.Printf("fetching UpCloud instance config from: %q ", UpCloudMetadataEndpoint) - - metaConfigDl, err := download.Download(ctx, UpCloudMetadataEndpoint) - if err != nil { - return nil, fmt.Errorf("failed to fetch network config from metadata service") - } - log.Printf("fetching machine config from: %q", UpCloudUserDataEndpoint) - machineConfigDl, err := download.Download(ctx, UpCloudUserDataEndpoint, + return download.Download(ctx, UpCloudUserDataEndpoint, download.WithErrorOnNotFound(errors.ErrNoConfigSource), download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, err - } - - confProvider, err := configloader.NewFromBytes(machineConfigDl) - if err != nil { - return nil, err - } - - confProvider, err = u.ConfigurationNetwork(metaConfigDl, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() } // Mode implements the runtime.Platform interface. @@ -168,37 +207,35 @@ func (u *UpCloud) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (u *UpCloud) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", UpCloudHostnameEndpoint) - - host, err := download.Download(ctx, UpCloudHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - return nil, err - } - - return host, nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (u *UpCloud) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - log.Printf("fetching external IP from: %q", UpCloudExternalIPEndpoint) - - exIP, err := download.Download(ctx, UpCloudExternalIPEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil { - return addrs, err - } - - addrs = append(addrs, net.ParseIP(string(exIP))) - - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (u *UpCloud) KernelArgs() procfs.Parameters { return []*procfs.Parameter{} } + +// NetworkConfiguration implements the runtime.Platform interface. +func (u *UpCloud) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching UpCloud instance config from: %q ", UpCloudMetadataEndpoint) + + metaConfigDl, err := download.Download(ctx, UpCloudMetadataEndpoint) + if err != nil { + return fmt.Errorf("failed to fetch network config from metadata service: %w", err) + } + + meta := &MetaData{} + if err = json.Unmarshal(metaConfigDl, meta); err != nil { + return err + } + + networkConfig, err := u.ParseMetadata(meta) + if err != nil { + return err + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go index 9831a8905..e74a37362 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud/upcloud_test.go @@ -5,135 +5,35 @@ package upcloud_test import ( + _ "embed" + "encoding/json" "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/upcloud" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} +//go:embed testdata/metadata.json +var rawMetadata []byte -func (suite *ConfigSuite) TestNetworkConfig() { - cfg := []byte(`{ -"cloud_name": "upcloud", -"instance_id": "00123456-1111-2222-3333-123456789012", -"hostname": "talos", -"network": { - "interfaces": [ - { - "index": 1, - "ip_addresses": [ - { - "address": "185.70.197.2", - "dhcp": true, - "dns": [ - "94.237.127.9", - "94.237.40.9" - ], - "family": "IPv4", - "floating": false, - "gateway": "185.70.196.1", - "network": "185.70.196.0/22" - }, - { - "address": "185.70.197.3", - "dhcp": false, - "dns": null, - "family": "IPv4", - "floating": true, - "gateway": "", - "network": "185.70.197.3/32" - } - ], - "mac": "5e:bf:5e:02:28:07", - "network_id": "035ef879-1111-2222-3333-123456789012", - "type": "public" - }, - { - "index": 2, - "ip_addresses": [ - { - "address": "10.11.0.2", - "dhcp": true, - "dns": null, - "family": "IPv4", - "floating": false, - "gateway": "10.11.0.1", - "network": "10.11.0.0/22" - } - ], - "mac": "5e:bf:5e:02:cd:e0", - "network_id": "031c9f9c-1111-2222-3333-123456789012", - "type": "utility" - }, - { - "index": 3, - "ip_addresses": [ - { - "address": "2a04:3544:8000:1000:0000:1111:2222:3333", - "dhcp": true, - "dns": [ - "2a04:3540:53::1", - "2a04:3544:53::1" - ], - "family": "IPv6", - "floating": false, - "gateway": "2a04:3544:8000:1000::1", - "network": "2a04:3544:8000:1000::/64" - } - ], - "mac": "5e:bf:5e:02:78:a4", - "network_id": "03b326a2-1111-2222-3333-123456789012", - "type": "public" - } - ], - "dns": [ - "94.237.127.9", - "94.237.40.9" - ] -}, -"storage": {}, -"tags": [], -"user_data": "", -"vendor_data": "" -}`) +//go:embed testdata/expected.yaml +var expectedNetworkConfig string +func TestParseMetadata(t *testing.T) { p := &upcloud.UpCloud{} - defaultMachineConfig := &v1alpha1.Config{} + var m upcloud.MetaData - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceAddresses: []string{"185.70.197.3/32"}, - DeviceDHCP: true, - }, - { - DeviceInterface: "eth1", - DeviceDHCP: true, - }, - { - DeviceInterface: "eth2", - DeviceDHCP: false, - }, - }, - }, - }, - } + require.NoError(t, json.Unmarshal(rawMetadata, &m)) - result, err := p.ConfigurationNetwork(cfg, defaultMachineConfig) + networkConfig, err := p.ParseMetadata(&m) + require.NoError(t, err) - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go index 425199b7e..eff289e4d 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_amd64.go @@ -14,7 +14,6 @@ import ( "errors" "fmt" "log" - "net" "github.com/talos-systems/go-procfs/procfs" "github.com/vmware/govmomi/ovf" @@ -186,21 +185,11 @@ func (v *VMware) Configuration(context.Context) ([]byte, error) { return nil, nil } -// Hostname implements the platform.Platform interface. -func (v *VMware) Hostname(context.Context) (hostname []byte, err error) { - return nil, nil -} - // Mode implements the platform.Platform interface. func (v *VMware) Mode() runtime.Mode { return runtime.ModeCloud } -// ExternalIPs implements the runtime.Platform interface. -func (v *VMware) ExternalIPs(context.Context) (addrs []net.IP, err error) { - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (v *VMware) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ @@ -208,3 +197,8 @@ func (v *VMware) KernelArgs() procfs.Parameters { procfs.NewParameter("earlyprintk").Append("ttyS0,115200"), } } + +// NetworkConfiguration implements the runtime.Platform interface. +func (v *VMware) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_other.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_other.go index 45157a993..b3f21a1be 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_other.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vmware/vmware_other.go @@ -10,7 +10,6 @@ package vmware import ( "context" "fmt" - "net" "github.com/talos-systems/go-procfs/procfs" @@ -30,22 +29,17 @@ func (v *VMware) Configuration(context.Context) ([]byte, error) { return nil, fmt.Errorf("arch not supported") } -// Hostname implements the platform.Platform interface. -func (v *VMware) Hostname(context.Context) (hostname []byte, err error) { - return nil, fmt.Errorf("arch not supported") -} - // Mode implements the platform.Platform interface. func (v *VMware) Mode() runtime.Mode { return runtime.ModeCloud } -// ExternalIPs implements the runtime.Platform interface. -func (v *VMware) ExternalIPs(context.Context) (addrs []net.IP, err error) { - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (v *VMware) KernelArgs() procfs.Parameters { return []*procfs.Parameter{} } + +// NetworkConfiguration implements the runtime.Platform interface. +func (v *VMware) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml new file mode 100644 index 000000000..834890867 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/expected.yaml @@ -0,0 +1,38 @@ +addresses: + - address: 10.7.96.0/20 + linkName: eth1 + family: inet4 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + logical: false + up: true + mtu: 0 + kind: "" + type: netrom + layer: platform + - name: eth1 + logical: false + up: true + mtu: 1450 + kind: "" + type: netrom + layer: platform +routes: [] +hostnames: + - hostname: talos + domainname: "" + layer: platform +resolvers: [] +timeServers: [] +operators: + - operator: dhcp4 + linkName: eth0 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: + - 1.2.3.4 diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json new file mode 100644 index 000000000..fd4097564 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/testdata/metadata.json @@ -0,0 +1,61 @@ +{ + "bgp": { + "ipv4": { + "my-address": "", + "my-asn": "", + "peer-address": "", + "peer-asn": "" + }, + "ipv6": { + "my-address": "", + "my-asn": "", + "peer-address": "", + "peer-asn": "" + } + }, + "hostname": "talos", + "instance-v2-id": "91b07056-af72-4551-b15b-d57d34071be9", + "instanceid": "50190000", + "interfaces": [ + { + "ipv4": { + "additional": [], + "address": "95.111.222.111", + "gateway": "95.111.222.1", + "netmask": "255.255.254.0" + }, + "ipv6": { + "additional": [], + "address": "2001:19f0:5001:2095:1111:2222:3333:4444", + "network": "2001:19f0:5001:2095::", + "prefix": "64" + }, + "mac": "56:00:03:89:53:e0", + "network-type": "public" + }, + { + "ipv4": { + "additional": [], + "address": "10.7.96.3", + "gateway": "", + "netmask": "255.255.240.0" + }, + "ipv6": { + "additional": [], + "network": "", + "prefix": "" + }, + "mac": "5a:00:03:89:53:e0", + "network-type": "private", + "network-v2-id": "dadc2b30-0b55-4fa1-8c29-f67215bd5ac4", + "networkid": "net6126811851cd7" + } + ], + "public-keys": [ + "ssh-ed25519" + ], + "region": { + "regioncode": "AMS" + }, + "user-defined": [] +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go index 4d0b5870c..3d4f269a7 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr.go @@ -7,19 +7,19 @@ package vultr import ( "context" "encoding/json" + stderrors "errors" "fmt" "log" - "net" "github.com/talos-systems/go-procfs/procfs" "github.com/vultr/metadata" + "inet.af/netaddr" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors" "github.com/talos-systems/talos/pkg/download" - "github.com/talos-systems/talos/pkg/machinery/config" - "github.com/talos-systems/talos/pkg/machinery/config/configloader" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/nethelpers" + "github.com/talos-systems/talos/pkg/machinery/resources/network" ) const ( @@ -41,87 +41,96 @@ func (v *Vultr) Name() string { return "vultr" } -// ConfigurationNetwork implements the network configuration interface. -func (v *Vultr) ConfigurationNetwork(metadataConfig []byte, confProvider config.Provider) (config.Provider, error) { - var machineConfig *v1alpha1.Config +// ParseMetadata converts Vultr platform metadata into platform network config. +// +//nolint:gocyclo +func (v *Vultr) ParseMetadata(meta *metadata.MetaData, extIP []byte) (*runtime.PlatformNetworkConfig, error) { + networkConfig := &runtime.PlatformNetworkConfig{} - machineConfig, ok := confProvider.(*v1alpha1.Config) - if !ok { - return nil, fmt.Errorf("unable to determine machine config type") + if ip, err := netaddr.ParseIP(string(extIP)); err == nil { + networkConfig.ExternalIPs = append(networkConfig.ExternalIPs, ip) } - meta := &metadata.MetaData{} - if err := json.Unmarshal(metadataConfig, meta); err != nil { - return nil, err + if meta.Hostname != "" { + hostnameSpec := network.HostnameSpecSpec{ + ConfigLayer: network.ConfigPlatform, + } + + if err := hostnameSpec.ParseFQDN(meta.Hostname); err != nil { + return nil, err + } + + networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec) } - if machineConfig.MachineConfig == nil { - machineConfig.MachineConfig = &v1alpha1.MachineConfig{} - } + for i, addr := range meta.Interfaces { + iface := fmt.Sprintf("eth%d", i) - if machineConfig.MachineConfig.MachineNetwork == nil { - machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{} - } + link := network.LinkSpecSpec{ + Name: iface, + Up: true, + ConfigLayer: network.ConfigPlatform, + } - if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil { - for i, addr := range meta.Interfaces { - iface := &v1alpha1.Device{ - DeviceInterface: fmt.Sprintf("eth%d", i), - } + if addr.NetworkType == "private" { + link.MTU = 1450 + } - if addr.IPv4.Address != "" { - iface.DeviceDHCP = true - } + networkConfig.Links = append(networkConfig.Links, link) - if addr.NetworkType == "private" { - iface.DeviceMTU = 1450 - - if addr.IPv4.Address != "" { - mask, _ := net.IPMask(net.ParseIP(addr.IPv4.Netmask).To4()).Size() - - iface.DeviceDHCP = false - iface.DeviceAddresses = append(iface.DeviceAddresses, - fmt.Sprintf("%s/%d", addr.IPv4.Address, mask), - ) + if addr.IPv4.Address != "" { + if addr.NetworkType != "private" { + networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: iface, + RequireUp: true, + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + ConfigLayer: network.ConfigPlatform, + }) + } else { + maskIP, err := netaddr.ParseIP(addr.IPv4.Netmask) + if err != nil { + return nil, err } - } - machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces, iface) + mask, _ := maskIP.MarshalBinary() //nolint:errcheck // never fails + + ip, err := netaddr.ParseIP(addr.IPv4.Address) + if err != nil { + return nil, err + } + + ipAddr, err := ip.Netmask(mask) + if err != nil { + return nil, err + } + + networkConfig.Addresses = append(networkConfig.Addresses, + network.AddressSpecSpec{ + ConfigLayer: network.ConfigPlatform, + LinkName: iface, + Address: ipAddr, + Scope: nethelpers.ScopeGlobal, + Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent), + Family: nethelpers.FamilyInet4, + }, + ) + } } } - return confProvider, nil + return networkConfig, nil } // Configuration implements the runtime.Platform interface. func (v *Vultr) Configuration(ctx context.Context) ([]byte, error) { - log.Printf("fetching Vultr instance config from: %q ", VultrMetadataEndpoint) - - metaConfigDl, err := download.Download(ctx, VultrMetadataEndpoint) - if err != nil { - return nil, errors.ErrNoConfigSource - } - log.Printf("fetching machine config from: %q", VultrUserDataEndpoint) - machineConfigDl, err := download.Download(ctx, VultrUserDataEndpoint, + return download.Download(ctx, VultrUserDataEndpoint, download.WithErrorOnNotFound(errors.ErrNoConfigSource), download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource)) - if err != nil { - return nil, err - } - - confProvider, err := configloader.NewFromBytes(machineConfigDl) - if err != nil { - return nil, err - } - - confProvider, err = v.ConfigurationNetwork(metaConfigDl, confProvider) - if err != nil { - return nil, err - } - - return confProvider.Bytes() } // Mode implements the runtime.Platform interface. @@ -129,39 +138,42 @@ func (v *Vultr) Mode() runtime.Mode { return runtime.ModeCloud } -// Hostname implements the runtime.Platform interface. -func (v *Vultr) Hostname(ctx context.Context) (hostname []byte, err error) { - log.Printf("fetching hostname from: %q", VultrHostnameEndpoint) - - hostname, err = download.Download(ctx, VultrHostnameEndpoint, - download.WithErrorOnNotFound(errors.ErrNoHostname), - download.WithErrorOnEmptyResponse(errors.ErrNoHostname)) - if err != nil { - return nil, err - } - - return hostname, nil -} - -// ExternalIPs implements the runtime.Platform interface. -func (v *Vultr) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) { - log.Printf("fetching external IP from: %q", VultrExternalIPEndpoint) - - exIP, err := download.Download(ctx, VultrExternalIPEndpoint, - download.WithErrorOnNotFound(errors.ErrNoExternalIPs), - download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) - if err != nil { - return nil, err - } - - if addr := net.ParseIP(string(exIP)); addr != nil { - addrs = append(addrs, addr) - } - - return addrs, err -} - // KernelArgs implements the runtime.Platform interface. func (v *Vultr) KernelArgs() procfs.Parameters { return []*procfs.Parameter{} } + +// NetworkConfiguration implements the runtime.Platform interface. +func (v *Vultr) NetworkConfiguration(ctx context.Context, ch chan<- *runtime.PlatformNetworkConfig) error { + log.Printf("fetching Vultr instance config from: %q ", VultrMetadataEndpoint) + + metaConfigDl, err := download.Download(ctx, VultrMetadataEndpoint) + if err != nil { + return fmt.Errorf("error fetching metadata: %w", err) + } + + meta := &metadata.MetaData{} + if err = json.Unmarshal(metaConfigDl, meta); err != nil { + return err + } + + extIP, err := download.Download(ctx, VultrExternalIPEndpoint, + download.WithErrorOnNotFound(errors.ErrNoExternalIPs), + download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs)) + if err != nil && !stderrors.Is(err, errors.ErrNoExternalIPs) { + return err + } + + networkConfig, err := v.ParseMetadata(meta, extIP) + if err != nil { + return err + } + + select { + case ch <- networkConfig: + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go index 7961e6104..00fd3acc0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr/vultr_test.go @@ -5,53 +5,36 @@ package vultr_test import ( + _ "embed" + "encoding/json" "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vultr/metadata" + "gopkg.in/yaml.v3" "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/vultr" - "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" ) -type ConfigSuite struct { - suite.Suite -} +//go:embed testdata/metadata.json +var rawMetadata []byte -func (suite *ConfigSuite) TestNetworkConfig() { - //nolint:lll - cfg := []byte(`{ -"bgp":{"ipv4":{"my-address":"","my-asn":"","peer-address":"","peer-asn":""},"ipv6":{"my-address":"","my-asn":"","peer-address":"","peer-asn":""}},"hostname":"talos","instance-v2-id":"91b07056-af72-4551-b15b-d57d34071be9","instanceid":"50190000","interfaces":[{"ipv4":{"additional":[],"address":"95.111.222.111","gateway":"95.111.222.1","netmask":"255.255.254.0"},"ipv6":{"additional":[],"address":"2001:19f0:5001:2095:1111:2222:3333:4444","network":"2001:19f0:5001:2095::","prefix":"64"},"mac":"56:00:03:89:53:e0","network-type":"public"},{"ipv4":{"additional":[],"address":"10.7.96.3","gateway":"","netmask":"255.255.240.0"},"ipv6":{"additional":[],"network":"","prefix":""},"mac":"5a:00:03:89:53:e0","network-type":"private","network-v2-id":"dadc2b30-0b55-4fa1-8c29-f67215bd5ac4","networkid":"net6126811851cd7"}],"public-keys":["ssh-ed25519"],"region":{"regioncode":"AMS"},"user-defined":[] -}`) +//go:embed testdata/expected.yaml +var expectedNetworkConfig string +func TestParseMetadata(t *testing.T) { p := &vultr.Vultr{} - defaultMachineConfig := &v1alpha1.Config{} + var m metadata.MetaData - machineConfig := &v1alpha1.Config{ - MachineConfig: &v1alpha1.MachineConfig{ - MachineNetwork: &v1alpha1.NetworkConfig{ - NetworkInterfaces: []*v1alpha1.Device{ - { - DeviceInterface: "eth0", - DeviceDHCP: true, - }, - { - DeviceInterface: "eth1", - DeviceAddresses: []string{"10.7.96.3/20"}, - DeviceDHCP: false, - DeviceMTU: 1450, - }, - }, - }, - }, - } + require.NoError(t, json.Unmarshal(rawMetadata, &m)) - result, err := p.ConfigurationNetwork(cfg, defaultMachineConfig) + networkConfig, err := p.ParseMetadata(&m, []byte("1.2.3.4")) + require.NoError(t, err) - suite.Require().NoError(err) - suite.Assert().Equal(machineConfig, result) -} - -func TestConfigSuite(t *testing.T) { - suite.Run(t, new(ConfigSuite)) + marshaled, err := yaml.Marshal(networkConfig) + require.NoError(t, err) + + assert.Equal(t, expectedNetworkConfig, string(marshaled)) } diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index 6e16e3dc5..fd1be3298 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -154,6 +154,7 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error &network.OperatorConfigController{ Cmdline: procfs.ProcCmdline(), }, + &network.OperatorMergeController{}, &network.OperatorSpecController{ V1alpha1Platform: ctrl.v1alpha1Runtime.State().Platform(), State: ctrl.v1alpha1Runtime.State().V1Alpha2().Resources(), diff --git a/pkg/machinery/config/dynamic.go b/pkg/machinery/config/dynamic.go deleted file mode 100644 index 9018c18e7..000000000 --- a/pkg/machinery/config/dynamic.go +++ /dev/null @@ -1,16 +0,0 @@ -// 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 config - -import ( - "context" - "net" -) - -// DynamicConfigProvider provides additional configuration which is overlaid on top of existing configuration. -type DynamicConfigProvider interface { - Hostname(context.Context) ([]byte, error) - ExternalIPs(context.Context) ([]net.IP, error) -} diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 4c892b800..39c529b88 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -568,6 +568,9 @@ const ( // SideroLinkDefaultPeerKeepalive is the interval at which Wireguard Peer Keepalives should be sent. SideroLinkDefaultPeerKeepalive = 25 * time.Second + + // PlatformNetworkConfigFilename is the filename to cache platform network configuration reboots. + PlatformNetworkConfigFilename = "platform-network.yaml" ) // See https://linux.die.net/man/3/klogctl diff --git a/pkg/machinery/resources/network/operator.go b/pkg/machinery/resources/network/operator.go index d83514287..2273419cf 100644 --- a/pkg/machinery/resources/network/operator.go +++ b/pkg/machinery/resources/network/operator.go @@ -4,7 +4,7 @@ package network -//go:generate stringer -type=Operator -linecomment +//go:generate enumer -type=Operator -linecomment -text // Operator enumerates Talos network operators. type Operator int @@ -15,8 +15,3 @@ const ( OperatorDHCP6 // dhcp6 OperatorVIP // vip ) - -// MarshalYAML implements yaml.Marshaler. -func (operator Operator) MarshalYAML() (interface{}, error) { - return operator.String(), nil -} diff --git a/pkg/machinery/resources/network/operator_enumer.go b/pkg/machinery/resources/network/operator_enumer.go new file mode 100644 index 000000000..f81475047 --- /dev/null +++ b/pkg/machinery/resources/network/operator_enumer.go @@ -0,0 +1,63 @@ +// Code generated by "enumer -type=Operator -linecomment -text"; DO NOT EDIT. + +// +package network + +import ( + "fmt" +) + +const _OperatorName = "dhcp4dhcp6vip" + +var _OperatorIndex = [...]uint8{0, 5, 10, 13} + +func (i Operator) String() string { + if i < 0 || i >= Operator(len(_OperatorIndex)-1) { + return fmt.Sprintf("Operator(%d)", i) + } + return _OperatorName[_OperatorIndex[i]:_OperatorIndex[i+1]] +} + +var _OperatorValues = []Operator{0, 1, 2} + +var _OperatorNameToValueMap = map[string]Operator{ + _OperatorName[0:5]: 0, + _OperatorName[5:10]: 1, + _OperatorName[10:13]: 2, +} + +// OperatorString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func OperatorString(s string) (Operator, error) { + if val, ok := _OperatorNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Operator values", s) +} + +// OperatorValues returns all values of the enum +func OperatorValues() []Operator { + return _OperatorValues +} + +// IsAOperator returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Operator) IsAOperator() bool { + for _, v := range _OperatorValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for Operator +func (i Operator) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for Operator +func (i *Operator) UnmarshalText(text []byte) error { + var err error + *i, err = OperatorString(string(text)) + return err +} diff --git a/pkg/machinery/resources/network/operator_spec.go b/pkg/machinery/resources/network/operator_spec.go index a2a476ef3..49301993d 100644 --- a/pkg/machinery/resources/network/operator_spec.go +++ b/pkg/machinery/resources/network/operator_spec.go @@ -30,6 +30,8 @@ type OperatorSpecSpec struct { DHCP4 DHCP4OperatorSpec `yaml:"dhcp4,omitempty"` DHCP6 DHCP6OperatorSpec `yaml:"dhcp6,omitempty"` VIP VIPOperatorSpec `yaml:"vip,omitempty"` + + ConfigLayer ConfigLayer `yaml:"layer"` } // DHCP4OperatorSpec describes DHCP4 operator options. diff --git a/pkg/machinery/resources/network/operator_spec_test.go b/pkg/machinery/resources/network/operator_spec_test.go new file mode 100644 index 000000000..b2a688a3b --- /dev/null +++ b/pkg/machinery/resources/network/operator_spec_test.go @@ -0,0 +1,78 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "inet.af/netaddr" + + "github.com/talos-systems/talos/pkg/machinery/resources/network" +) + +func TestOperatorSpecMarshalYAML(t *testing.T) { + spec := network.OperatorSpecSpec{ + Operator: network.OperatorDHCP4, + LinkName: "eth0", + RequireUp: true, + + DHCP4: network.DHCP4OperatorSpec{ + RouteMetric: 1024, + }, + DHCP6: network.DHCP6OperatorSpec{ + RouteMetric: 1024, + }, + VIP: network.VIPOperatorSpec{ + IP: netaddr.MustParseIP("192.168.1.1"), + GratuitousARP: true, + EquinixMetal: network.VIPEquinixMetalSpec{ + ProjectID: "a", + DeviceID: "b", + APIToken: "c", + }, + HCloud: network.VIPHCloudSpec{ + DeviceID: 3, + NetworkID: 4, + APIToken: "d", + }, + }, + ConfigLayer: network.ConfigMachineConfiguration, + } + + marshaled, err := yaml.Marshal(spec) + require.NoError(t, err) + + assert.Equal(t, + `operator: dhcp4 +linkName: eth0 +requireUp: true +dhcp4: + routeMetric: 1024 +dhcp6: + routeMetric: 1024 +vip: + ip: 192.168.1.1 + gratuitousARP: true + equinixMetal: + projectID: a + deviceID: b + apiToken: c + hcloud: + deviceID: 3 + networkID: 4 + apiToken: d +layer: configuration +`, + string(marshaled)) + + var spec2 network.OperatorSpecSpec + + require.NoError(t, yaml.Unmarshal(marshaled, &spec2)) + + assert.Equal(t, spec, spec2) +} diff --git a/pkg/machinery/resources/network/operator_string.go b/pkg/machinery/resources/network/operator_string.go deleted file mode 100644 index 880971623..000000000 --- a/pkg/machinery/resources/network/operator_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=Operator -linecomment"; DO NOT EDIT. - -package network - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[OperatorDHCP4-0] - _ = x[OperatorDHCP6-1] - _ = x[OperatorVIP-2] -} - -const _Operator_name = "dhcp4dhcp6vip" - -var _Operator_index = [...]uint8{0, 5, 10, 13} - -func (i Operator) String() string { - if i < 0 || i >= Operator(len(_Operator_index)-1) { - return "Operator(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Operator_name[_Operator_index[i]:_Operator_index[i+1]] -}