Skip to content

Commit

Permalink
feat: azure platform ipv6 support
Browse files Browse the repository at this point in the history
If network in machineconfig is not defined, Talos checks ipv6
capabilities and updates network config.

Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev>
Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
sergelogvinov authored and smira committed Oct 20, 2021
1 parent d32814e commit 666a2b6
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 130 deletions.
235 changes: 136 additions & 99 deletions internal/app/machined/pkg/runtime/v1alpha1/platform/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,50 @@ import (
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"regexp"

"github.com/AlekSi/pointer"
"github.com/talos-systems/go-procfs/procfs"
"golang.org/x/sys/unix"

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

const (
// AzureUserDataEndpoint is the local endpoint for the config.
// By specifying format=text and drilling down to the actual key we care about
// we get a base64 encoded config response.
AzureUserDataEndpoint = "http://169.254.169.254/metadata/instance/compute/customData?api-version=2019-06-01&format=text"
// AzureHostnameEndpoint is the local endpoint for the hostname.
AzureHostnameEndpoint = "http://169.254.169.254/metadata/instance/compute/name?api-version=2019-06-01&format=text"
// AzureInternalEndpoint is the Azure Internal Channel IP
// https://blogs.msdn.microsoft.com/mast/2015/05/18/what-is-the-ip-address-168-63-129-16/
AzureInternalEndpoint = "http://168.63.129.16"
// AzureHostnameEndpoint is the local endpoint for the hostname.
AzureHostnameEndpoint = "http://169.254.169.254/metadata/instance/compute/name?api-version=2021-05-01&format=text"
// AzureInterfacesEndpoint is the local endpoint to get external IPs.
AzureInterfacesEndpoint = "http://169.254.169.254/metadata/instance/network/interface?api-version=2019-06-01"
AzureInterfacesEndpoint = "http://169.254.169.254/metadata/instance/network/interface?api-version=2021-05-01"

mnt = "/mnt"
)

// NetworkConfig holds network interface meta config.
type NetworkConfig struct {
IPv4 struct {
IPAddresses []IPAddresses `json:"ipAddress"`
} `json:"ipv4"`
IPv6 struct {
IPAddresses []IPAddresses `json:"ipAddress"`
} `json:"ipv6"`
}

// IPAddresses holds public/private IPs.
type IPAddresses struct {
PrivateIPAddress string `json:"privateIpAddress"`
PublicIPAddress string `json:"publicIpAddress"`
}

// Azure is the concrete type that implements the platform.Platform interface.
type Azure struct{}

Expand All @@ -55,133 +71,129 @@ func (a *Azure) Name() string {
return "azure"
}

// Configuration implements the platform.Platform interface.
func (a *Azure) Configuration(ctx context.Context) ([]byte, error) {
// TODO: support ErrNoConfigSource, requires handling of both CD-ROM & user-data sources
// requires splitting `linuxAgent` into separate platform task which is called when node is up (or close to that)
// attempt to download from metadata endpoint
// disabled by default
log.Printf("fetching machine config from: %q", AzureUserDataEndpoint)
// ConfigurationNetwork implements the network configuration interface.
func (a *Azure) ConfigurationNetwork(metadataNetworkConfig []byte, confProvider config.Provider) (config.Provider, error) {
var machineConfig *v1alpha1.Config

config, err := download.Download(ctx, AzureUserDataEndpoint, download.WithHeaders(map[string]string{"Metadata": "true"}), download.WithFormat("base64"))
if err != nil {
fmt.Printf("metadata download failed, falling back to ovf-env.xml file. err: %s", err.Error())
machineConfig, ok := confProvider.(*v1alpha1.Config)
if !ok {
return nil, fmt.Errorf("unable to determine machine config type")
}

// fall back to cdrom read b/c we failed to pull userdata from metadata server
if len(config) == 0 {
log.Printf("fetching machine config from: ovf-env.xml")
if machineConfig.MachineConfig == nil {
machineConfig.MachineConfig = &v1alpha1.MachineConfig{}
}

config, err = a.configFromCD()
if err != nil {
return nil, err
}
if machineConfig.MachineConfig.MachineNetwork == nil {
machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{}
}

if err := linuxAgent(ctx); err != nil {
var interfaceAddresses []NetworkConfig

if err := json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil {
return nil, err
}

return config, nil
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)},
}

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

return confProvider, nil
}

// Hostname implements the platform.Platform interface.
func (a *Azure) Hostname(ctx context.Context) (hostname []byte, err error) {
var (
req *http.Request
resp *http.Response
)
// Configuration implements the platform.Platform interface.
func (a *Azure) Configuration(ctx context.Context) ([]byte, error) {
defer func() {
if err := linuxAgent(ctx); err != nil {
log.Printf("failed to update instance status, err: %s", err.Error())
}
}()

log.Printf("fetching network config from %q", AzureInterfacesEndpoint)

req, err = http.NewRequestWithContext(ctx, "GET", AzureHostnameEndpoint, nil)
metadataNetworkConfig, err := download.Download(ctx, AzureInterfacesEndpoint,
download.WithHeaders(map[string]string{"Metadata": "true"}))
if err != nil {
return
return nil, fmt.Errorf("failed to fetch network config from metadata service")
}

req.Header.Add("Metadata", "true")

client := &http.Client{}
log.Printf("fetching machine config from ovf-env.xml")

resp, err = client.Do(req)
// Custom data is not available in IMDS, so trying to find it on CDROM.
machineConfig, err := a.configFromCD()
if err != nil {
return
log.Printf("fetching machine config from cdrom failed, err: %s", err.Error())

return nil, err
}

//nolint:errcheck
defer resp.Body.Close()
confProvider, err := configloader.NewFromBytes(machineConfig)
if err != nil {
return nil, fmt.Errorf("error parsing machine config: %w", err)
}

if resp.StatusCode != http.StatusOK {
return hostname, fmt.Errorf("failed to fetch hostname from metadata service: %d", resp.StatusCode)
confProvider, err = a.ConfigurationNetwork(metadataNetworkConfig, confProvider)
if err != nil {
return nil, err
}

return ioutil.ReadAll(resp.Body)
return confProvider.Bytes()
}

// Mode implements the platform.Platform interface.
func (a *Azure) Mode() runtime.Mode {
return runtime.ModeCloud
}

// ExternalIPs implements the runtime.Platform interface.
func (a *Azure) 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", AzureInterfacesEndpoint, nil); err != nil {
return
}

req.Header.Add("Metadata", "true")

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")
}
// Hostname implements the platform.Platform interface.
func (a *Azure) Hostname(ctx context.Context) (hostname []byte, err error) {
log.Printf("fetching hostname from: %q", AzureHostnameEndpoint)

if body, err = ioutil.ReadAll(resp.Body); err != nil {
return addrs, err
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
}

type IPAddress struct {
PrivateIPAddress string `json:"privateIpAddress"`
PublicIPAddress string `json:"publicIpAddress"`
}
return host, nil
}

type interfaces []struct {
IPv4 struct {
IPAddresses []IPAddress `json:"ipAddress"`
} `json:"ipv4"`
IPv6 struct {
IPAddresses []IPAddress `json:"ipAddress"`
} `json:"ipv6"`
}
// 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)

interfaceAddresses := interfaces{}
if err = json.Unmarshal(body, &interfaceAddresses); err != nil {
return addrs, err
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
}

for _, iface := range interfaceAddresses {
for _, ipv4addr := range iface.IPv4.IPAddresses {
addrs = append(addrs, net.ParseIP(ipv4addr.PublicIPAddress))
}

for _, ipv6addr := range iface.IPv6.IPAddresses {
addrs = append(addrs, net.ParseIP(ipv6addr.PublicIPAddress))
}
addrs, err = a.getPublicIPs(metadataNetworkConfig)
if err != nil {
return nil, err
}

return addrs, err
return addrs, nil
}

// KernelArgs implements the runtime.Platform interface.
Expand All @@ -193,7 +205,7 @@ func (a *Azure) KernelArgs() procfs.Parameters {
}
}

// configFromCD handles looking for devices and trying to mount/fetch xml to get the userdata.
// configFromCD handles looking for devices and trying to mount/fetch xml to get the custom data.
func (a *Azure) configFromCD() ([]byte, error) {
devList, err := ioutil.ReadDir("/dev")
if err != nil {
Expand Down Expand Up @@ -244,5 +256,30 @@ func (a *Azure) configFromCD() ([]byte, error) {
}
}

return nil, fmt.Errorf("no devices seemed to contain ovf-env.xml for pulling machine config")
return nil, errors.ErrNoConfigSource
}

// getPublicIPs parced network metadata response.
func (a *Azure) getPublicIPs(metadataNetworkConfig []byte) (addrs []net.IP, err error) {
var interfaceAddresses []NetworkConfig

if err = json.Unmarshal(metadataNetworkConfig, &interfaceAddresses); err != nil {
return nil, errors.ErrNoExternalIPs
}

for _, iface := range interfaceAddresses {
for _, ipv4addr := range iface.IPv4.IPAddresses {
if ip := net.ParseIP(ipv4addr.PublicIPAddress); ip != nil {
addrs = append(addrs, ip)
}
}

for _, ipv6addr := range iface.IPv6.IPAddresses {
if ip := net.ParseIP(ipv6addr.PublicIPAddress); ip != nil {
addrs = append(addrs, ip)
}
}
}

return addrs, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,74 @@

package azure_test

import "testing"
import (
"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/AlekSi/pointer"
"github.com/stretchr/testify/suite"

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

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"
}
]
`)
a := &azure.Azure{}

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

0 comments on commit 666a2b6

Please sign in to comment.