Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Starting with this release, ignition-validate binaries are signed with the
- Support partitioning disk with mounted partitions
- Support Proxmox VE
- Support gzipped Akamai user_data
- Support IPv6 for single-stack OpenStack

### Changes

Expand Down
71 changes: 65 additions & 6 deletions internal/providers/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ package openstack

import (
"context"
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
Expand All @@ -44,10 +47,18 @@ const (
)

var (
metadataServiceUrl = url.URL{
Scheme: "http",
Host: "169.254.169.254",
Path: "openstack/latest/user_data",
userdataURLs = map[string]url.URL{
resource.IPv4: {
Scheme: "http",
Host: "169.254.169.254",
Path: "openstack/latest/user_data",
},

resource.IPv6: {
Scheme: "http",
Host: "[fe80::a9fe:a9fe%iface]",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are missing a bit of the ipv6

[fe80::a9fe:a9fe%25iface]

Path: "openstack/latest/user_data",
},
}
)

Expand Down Expand Up @@ -167,13 +178,61 @@ func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string)
}

func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) {
res, err := f.FetchToBuffer(metadataServiceUrl, resource.FetchOptions{})
urls := map[string]url.URL{
string(resource.IPv4): userdataURLs[resource.IPv4],
}

ifaceName, err := findInterfaceWithIPv6()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now since we have a list of strings, rather then 1 lets reach out to all of them at the same time.

I think we can do this a few ways, though I think it would be cool to do it in parallel using fan-out pattern.

Golang has a few concepts that will help us to achieve that.

goroutines, channels, waitgroups

if err == nil {
ipv6Url := userdataURLs[resource.IPv6]
ipv6Url.Host = strings.Replace(ipv6Url.Host, "iface", ifaceName, 1)
urls[string(resource.IPv6)] = ipv6Url
} else {
f.Logger.Info("No active IPv6 network interface found: %v", err)
}

cfg, _, err := resource.FetchConfigDualStack(
f,
urls,
func(f *resource.Fetcher, u url.URL) ([]byte, error) {
return f.FetchToBuffer(u, resource.FetchOptions{})
},
)

// the metadata server exists but doesn't contain any actual metadata,
// assume that there is no config specified
if err == resource.ErrNotFound {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we continue handling the not found error like it was done here?
I believe the same would need to happen for IPv6.

return nil, nil
}

return res, err
data, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
return data, nil
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to have some reference on why you are looking for the zone ID in the manner that you are. A simple link to the IPV6 metadata service docs should be good with a quick summary? wdyt?

func findInterfaceWithIPv6() (string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think it might be good to try all interfaces rather then just the first one we find. So lets return all IPV6 interfaces which are UP and not loopback. i.e return ( []string,error)

interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("error fetching network interfaces: %v", err)
}

for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}

addrs, err := iface.Addrs()
if err != nil {
continue
}

for _, addr := range addrs {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I get a small comment explaining how this is giving us the zoneID?

if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To16() != nil && ipnet.IP.To4() == nil {
return iface.Name, nil
}
}
}
return "", fmt.Errorf("no active IPv6 network interface found")
}
75 changes: 74 additions & 1 deletion internal/resource/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"io"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
Expand All @@ -36,9 +37,13 @@ import (
configErrors "github.com/coreos/ignition/v2/config/shared/errors"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/util"
"github.com/coreos/vcontext/report"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"

"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
providersUtil "github.com/coreos/ignition/v2/internal/providers/util"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/aws/aws-sdk-go/aws"
Expand All @@ -52,6 +57,11 @@ import (
"github.com/vincent-petithory/dataurl"
)

const (
IPv4 = "ipv4"
IPv6 = "ipv6"
)

var (
ErrSchemeUnsupported = errors.New("unsupported source scheme")
ErrPathNotAbsolute = errors.New("path is not absolute")
Expand Down Expand Up @@ -330,10 +340,17 @@ func (f *Fetcher) fetchFromHTTP(u url.URL, dest io.Writer, opts FetchOptions) er
p int
)

host := u.Hostname()
addr, _ := netip.ParseAddr(host)
network := "tcp6"
if addr.Is4() {
network = "tcp4"
}

// Assert that the port is not already used.
for {
p = opts.LocalPort()
l, err := net.Listen("tcp4", fmt.Sprintf(":%d", p))
l, err := net.Listen(network, fmt.Sprintf(":%d", p))
if err != nil && errors.Is(err, syscall.EADDRINUSE) {
continue
} else if err == nil {
Expand Down Expand Up @@ -725,3 +742,59 @@ func (f *Fetcher) parseARN(arnURL string) (string, string, string, string, error
key := strings.Join(urlSplit[1:], "/")
return bucket, key, "", regionHint, nil
}

// FetchConfigDualStack is a function that takes care of fetching Ignition configuration on systems where IPv4 only, IPv6 only or both are available.
// From a high level point of view, this function will try to fetch in parallel Ignition configuration from IPv4 and/or IPv6 - if both endpoints are available, it will
// return the first configuration successfully fetched.
func FetchConfigDualStack(f *Fetcher, userdataURLs map[string]url.URL, fetchConfig func(*Fetcher, url.URL) ([]byte, error)) (types.Config, report.Report, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

var (
err error
nbErrors int
)

// cfg holds the configuration for a given IP
cfg := make(map[url.URL][]byte)

// success hold the IP of the first successful configuration fetching
success := make(chan url.URL, 1)
errors := make(chan error, 2)

fetch := func(ctx context.Context, ip url.URL) {
d, e := fetchConfig(f, ip)
if e != nil {
f.Logger.Err("fetching configuration for %s: %v", ip.String(), e)
err = e
errors <- e
} else {
cfg[ip] = d
success <- ip
}
}

if ipv4, ok := userdataURLs[IPv4]; ok {
go fetch(ctx, ipv4)
}

if ipv6, ok := userdataURLs[IPv6]; ok {
go fetch(ctx, ipv6)
}

// Now wait for one success. (i.e wait for the first configuration to be available)
select {
case ip := <-success:
f.Logger.Debug("got configuration from: %s", ip.String())
return providersUtil.ParseConfig(f.Logger, cfg[ip])
case <-errors:
nbErrors++
if nbErrors == 2 {
f.Logger.Debug("all routines have failed to fetch configuration, returning last known error: %v", err)
return types.Config{}, report.Report{}, err
}
}

// we should never reach this line
return types.Config{}, report.Report{}, err
}
Loading