Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import (
stderrors "errors"
"fmt"
"log"
"net"
"net/netip"
"strings"
"time"

"github.com/cenkalti/backoff/v4"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/go-procfs/procfs"
Expand Down Expand Up @@ -48,16 +51,17 @@ func (o *OpenStack) ParseMetadata(
extIPs []netip.Addr,
metadata *MetadataConfig,
st state.State,
) (*runtime.PlatformNetworkConfig, error) {
) (*runtime.PlatformNetworkConfig, bool, error) {
networkConfig := &runtime.PlatformNetworkConfig{}
needsReconcile := false

if metadata.Hostname != "" {
hostnameSpec := network.HostnameSpecSpec{
ConfigLayer: network.ConfigPlatform,
}

if err := hostnameSpec.ParseFQDN(metadata.Hostname); err != nil {
return nil, err
return nil, false, err
}

networkConfig.Hostnames = append(networkConfig.Hostnames, hostnameSpec)
Expand All @@ -72,7 +76,7 @@ func (o *OpenStack) ParseMetadata(
if ip, err := netip.ParseAddr(netsvc.Address); err == nil {
dnsIPs = append(dnsIPs, ip)
} else {
return nil, fmt.Errorf("failed to parse dns service ip: %w", err)
return nil, false, fmt.Errorf("failed to parse dns service ip: %w", err)
}
}
}
Expand All @@ -86,7 +90,7 @@ func (o *OpenStack) ParseMetadata(

hostInterfaces, err := safe.StateListAll[*network.LinkStatus](ctx, st)
if err != nil {
return nil, fmt.Errorf("error listing host interfaces: %w", err)
return nil, false, fmt.Errorf("error listing host interfaces: %w", err)
}

ifaces := make(map[string]string)
Expand All @@ -103,12 +107,12 @@ func (o *OpenStack) ParseMetadata(

mode, err := nethelpers.BondModeByName(netLink.BondMode)
if err != nil {
return nil, fmt.Errorf("invalid bond_mode: %w", err)
return nil, false, fmt.Errorf("invalid bond_mode: %w", err)
}

hashPolicy, err := nethelpers.BondXmitHashPolicyByName(netLink.BondHashPolicy)
if err != nil {
return nil, fmt.Errorf("invalid bond_xmit_hash_policy: %w", err)
return nil, false, fmt.Errorf("invalid bond_xmit_hash_policy: %w", err)
}

bondName := fmt.Sprintf("bond%d", bondIndex)
Expand All @@ -132,6 +136,15 @@ func (o *OpenStack) ParseMetadata(
},
}

if netLink.Mac != "" {
mac, err := net.ParseMAC(netLink.Mac)
if err != nil {
return nil, false, fmt.Errorf("invalid bond MAC address %q: %w", netLink.Mac, err)
}

bondLink.HardwareAddress = nethelpers.HardwareAddr(mac)
}

if mode == nethelpers.BondMode8023AD {
bondLink.BondMaster.ADLACPActive = nethelpers.ADLACPActiveOn
}
Expand Down Expand Up @@ -178,18 +191,27 @@ func (o *OpenStack) ParseMetadata(
case "phy", "vif", "ovs", "bridge", "tap", "vhostuser", "hw_veb":
linkName := ""

for hostInterface := range hostInterfaces.All() {
if strings.EqualFold(hostInterface.TypedSpec().PermanentAddr.String(), netLink.Mac) {
linkName = hostInterface.Metadata().ID()
if netLink.Mac != "" {
for hostInterface := range hostInterfaces.All() {
macAddress := hostInterface.TypedSpec().PermanentAddr.String()
if macAddress == "" {
macAddress = hostInterface.TypedSpec().HardwareAddr.String()
}

if strings.EqualFold(macAddress, netLink.Mac) {
linkName = hostInterface.Metadata().ID()

break
break
}
}
}

if linkName == "" {
linkName = fmt.Sprintf("eth%d", idx)

log.Printf("failed to find interface with MAC %q, using %q", netLink.Mac, linkName)

needsReconcile = true
}

ifaces[netLink.ID] = linkName
Expand Down Expand Up @@ -290,7 +312,7 @@ func (o *OpenStack) ParseMetadata(
if ntwrk.Address != "" {
ipPrefix, err := address.IPPrefixFrom(ntwrk.Address, ntwrk.Netmask)
if err != nil {
return nil, fmt.Errorf("failed to parse ip address: %w", err)
return nil, false, fmt.Errorf("failed to parse ip address: %w", err)
}

family := nethelpers.FamilyInet4
Expand All @@ -312,7 +334,7 @@ func (o *OpenStack) ParseMetadata(
if ntwrk.Gateway != "" {
gw, err := netip.ParseAddr(ntwrk.Gateway)
if err != nil {
return nil, fmt.Errorf("failed to parse gateway ip: %w", err)
return nil, false, fmt.Errorf("failed to parse gateway ip: %w", err)
}

priority := uint32(network.DefaultRouteMetric)
Expand Down Expand Up @@ -341,12 +363,12 @@ func (o *OpenStack) ParseMetadata(
for _, route := range ntwrk.Routes {
gw, err := netip.ParseAddr(route.Gateway)
if err != nil {
return nil, fmt.Errorf("failed to parse route gateway: %w", err)
return nil, false, fmt.Errorf("failed to parse route gateway: %w", err)
}

dest, err := address.IPPrefixFrom(route.Network, route.Netmask)
if err != nil {
return nil, fmt.Errorf("failed to parse route network: %w", err)
return nil, false, fmt.Errorf("failed to parse route network: %w", err)
}

family := nethelpers.FamilyInet4
Expand Down Expand Up @@ -386,7 +408,7 @@ func (o *OpenStack) ParseMetadata(
ProviderID: fmt.Sprintf("openstack:///%s", metadata.UUID),
}

return networkConfig, nil
return networkConfig, needsReconcile, nil
}

// Configuration implements the runtime.Platform interface.
Expand Down Expand Up @@ -426,7 +448,14 @@ func (o *OpenStack) KernelArgs(string, quirks.Quirks) procfs.Parameters {
}

// NetworkConfiguration implements the runtime.Platform interface.
//
//nolint:gocyclo
func (o *OpenStack) NetworkConfiguration(ctx context.Context, st state.State, ch chan<- *runtime.PlatformNetworkConfig) error {
// wait for devices to be ready before proceeding, otherwise we might not find network interfaces by MAC
if err := netutils.WaitForDevicesReady(ctx, st); err != nil {
return fmt.Errorf("error waiting for devices to be ready: %w", err)
}

networkSource := false

metadataConfigDl, metadataNetworkConfigDl, _, err := o.configFromCD(ctx, st)
Expand Down Expand Up @@ -462,16 +491,36 @@ func (o *OpenStack) NetworkConfiguration(ctx context.Context, st state.State, ch
}
}

networkConfig, err := o.ParseMetadata(ctx, &unmarshalledNetworkConfig, extIPs, &meta, st)
if err != nil {
return err
}
// do a loop to retry network config remap in case of missing links
// on each try, export the configuration as it is, and if the network is reconciled next time, export the reconciled configuration
bckoff := backoff.NewExponentialBackOff()

select {
case ch <- networkConfig:
case <-ctx.Done():
return ctx.Err()
}
for {
networkConfig, needsReconcile, err := o.ParseMetadata(ctx, &unmarshalledNetworkConfig, extIPs, &meta, st)
if err != nil {
return err
}

select {
case ch <- networkConfig:
case <-ctx.Done():
return ctx.Err()
}

return nil
if !needsReconcile {
return nil
}

// wait for backoff to retry network config remap
nextBackoff := bckoff.NextBackOff()
if nextBackoff == backoff.Stop {
return nil
}

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(nextBackoff):
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package openstack_test

import (
"context"
_ "embed"
"encoding/json"
"net/netip"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v4"

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack"
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
Expand All @@ -32,47 +34,115 @@ var rawNetwork []byte
var expectedNetworkConfig string

func TestParseMetadata(t *testing.T) {
o := &openstack.OpenStack{}

var metadata openstack.MetadataConfig

require.NoError(t, json.Unmarshal(rawMetadata, &metadata))

var n openstack.NetworkConfig

require.NoError(t, json.Unmarshal(rawNetwork, &n))

ctx := t.Context()

st := state.WrapCore(namespaced.NewState(inmem.Build))

eth0 := network.NewLinkStatus(network.NamespaceName, "eth0")
eth0.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0xa4, 0xbf, 0x00, 0x10, 0x20, 0x30}
require.NoError(t, st.Create(ctx, eth0))

eth1 := network.NewLinkStatus(network.NamespaceName, "eth1")
eth1.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0xa4, 0xbf, 0x00, 0x10, 0x20, 0x31}
require.NoError(t, st.Create(ctx, eth1))

eth2 := network.NewLinkStatus(network.NamespaceName, "eth2")
eth2.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0xa4, 0xbf, 0x00, 0x10, 0x20, 0x33}
require.NoError(t, st.Create(ctx, eth2))

// Bond slaves

eth3 := network.NewLinkStatus(network.NamespaceName, "eth3")
eth3.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0x4c, 0xd9, 0x8f, 0xb3, 0x34, 0xf8}
require.NoError(t, st.Create(ctx, eth3))

eth4 := network.NewLinkStatus(network.NamespaceName, "eth4")
eth4.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0x4c, 0xd9, 0x8f, 0xb3, 0x34, 0xf7}
require.NoError(t, st.Create(ctx, eth4))

networkConfig, err := o.ParseMetadata(ctx, &n, []netip.Addr{netip.MustParseAddr("1.2.3.4")}, &metadata, st)
require.NoError(t, err)

marshaled, err := yaml.Marshal(networkConfig)
require.NoError(t, err)

assert.Equal(t, expectedNetworkConfig, string(marshaled))
t.Parallel()

for _, tt := range []struct {
name string
networkJSON []byte
metadataJSON []byte
extIPs []netip.Addr
setupState func(t *testing.T, ctx context.Context, st state.State)
expectedNeedsReconcile bool
expected string
checkResult func(t *testing.T, cfg *runtime.PlatformNetworkConfig)
}{
{
name: "full config",
networkJSON: rawNetwork,
metadataJSON: rawMetadata,
extIPs: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
setupState: func(t *testing.T, ctx context.Context, st state.State) {
eth0 := network.NewLinkStatus(network.NamespaceName, "eth0")
eth0.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0xa4, 0xbf, 0x00, 0x10, 0x20, 0x30}
require.NoError(t, st.Create(ctx, eth0))

eth1 := network.NewLinkStatus(network.NamespaceName, "eth1")
eth1.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0xa4, 0xbf, 0x00, 0x10, 0x20, 0x31}
require.NoError(t, st.Create(ctx, eth1))

eth2 := network.NewLinkStatus(network.NamespaceName, "eth2")
eth2.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0xa4, 0xbf, 0x00, 0x10, 0x20, 0x33}
require.NoError(t, st.Create(ctx, eth2))

eth3 := network.NewLinkStatus(network.NamespaceName, "eth3")
eth3.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0x4c, 0xd9, 0x8f, 0xb3, 0x34, 0xf8}
require.NoError(t, st.Create(ctx, eth3))

eth4 := network.NewLinkStatus(network.NamespaceName, "eth4")
eth4.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0x4c, 0xd9, 0x8f, 0xb3, 0x34, 0xf7}
require.NoError(t, st.Create(ctx, eth4))
},
expected: expectedNetworkConfig,
},
{
name: "HardwareAddr fallback",
networkJSON: []byte(`{"links":[{"id":"iface1","type":"phy","ethernet_mac_address":"aa:bb:cc:dd:ee:ff","mtu":1500}],"networks":[{"id":"net1","link":"iface1","type":"ipv4_dhcp"}]}`),
setupState: func(t *testing.T, ctx context.Context, st state.State) {
eth0 := network.NewLinkStatus(network.NamespaceName, "eth0")
eth0.TypedSpec().HardwareAddr = nethelpers.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
require.NoError(t, st.Create(ctx, eth0))
},
checkResult: func(t *testing.T, cfg *runtime.PlatformNetworkConfig) {
require.Len(t, cfg.Links, 1)
assert.Equal(t, "eth0", cfg.Links[0].Name)
},
},
{
name: "empty MAC does not match",
networkJSON: []byte(`{"links":[{"id":"iface1","type":"phy","ethernet_mac_address":"","mtu":1500}],"networks":[{"id":"net1","link":"iface1","type":"ipv4_dhcp"}]}`),
setupState: func(t *testing.T, ctx context.Context, st state.State) {
eth0 := network.NewLinkStatus(network.NamespaceName, "eth0")
require.NoError(t, st.Create(ctx, eth0))
},
expectedNeedsReconcile: true,
},
{
name: "MAC mismatch triggers reconcile",
networkJSON: []byte(`{"links":[{"id":"iface1","type":"phy","ethernet_mac_address":"aa:bb:cc:dd:ee:ff","mtu":1500}],"networks":[{"id":"net1","link":"iface1","type":"ipv4_dhcp"}]}`),
setupState: func(t *testing.T, ctx context.Context, st state.State) {
eth0 := network.NewLinkStatus(network.NamespaceName, "eth0")
eth0.TypedSpec().PermanentAddr = nethelpers.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
require.NoError(t, st.Create(ctx, eth0))
},
expectedNeedsReconcile: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

ctx := t.Context()
st := state.WrapCore(namespaced.NewState(inmem.Build))

tt.setupState(t, ctx, st)

var (
metadata openstack.MetadataConfig
n openstack.NetworkConfig
)

if tt.metadataJSON != nil {
require.NoError(t, json.Unmarshal(tt.metadataJSON, &metadata))
}

require.NoError(t, json.Unmarshal(tt.networkJSON, &n))

o := &openstack.OpenStack{}

networkConfig, needsReconcile, err := o.ParseMetadata(ctx, &n, tt.extIPs, &metadata, st)
require.NoError(t, err)

assert.Equal(t, tt.expectedNeedsReconcile, needsReconcile)

if tt.expected != "" {
marshaled, err := yaml.Marshal(networkConfig)
require.NoError(t, err)

assert.Equal(t, tt.expected, string(marshaled))
}

if tt.checkResult != nil {
tt.checkResult(t, networkConfig)
}
})
}
}
Loading