feat: inject platform network configuration as network resources

Instead of patching in the machine config, provide a new interface in
the platforms to supply platform-specific network configuration.

Configuration will be cached in the `/state` across reboots, and
configuration might be even updated in real time (e.g. on GCP).

Network configuration on platform level can be overridden with
user-supplied machine configuration.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
This commit is contained in:
Andrey Smirnov 2021-12-29 00:07:20 +03:00
parent 907f8cbfb8
commit 9ad5a67d21
No known key found for this signature in database
GPG Key ID: 7B26396447AB6DFD
67 changed files with 4809 additions and 3254 deletions

View File

@ -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))

View File

@ -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 (

View File

@ -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
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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", &regularTask{errCh: taskErr}, &regularTask{errCh: taskErr}))
// r.Add(phase.NewPhase("phase2", &regularTask{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{}, &regularTask{errCh: taskErr}, &nilTask{}))
// r.Add(phase.NewPhase("neverreached",
// &regularTask{}, // 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))
// }

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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

View File

@ -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"
}
]

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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: []

View File

@ -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: []

View File

@ -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'

View File

@ -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]

View File

@ -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

View File

@ -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
}

View File

@ -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"
}

View File

@ -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"
}
]
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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: []

View File

@ -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"
}
]

View File

@ -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
}

View File

@ -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))
}

View File

@ -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: []

View File

@ -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"
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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

View File

@ -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": ""
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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": []
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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(),

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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]]
}