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
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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
172 changes: 166 additions & 6 deletions internal/providers/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ package openstack

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

"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
Expand All @@ -44,10 +48,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]",
Path: "openstack/latest/user_data",
},
}
)

Expand Down Expand Up @@ -171,13 +183,161 @@ func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string)
}

func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) {
res, err := f.FetchToBuffer(metadataServiceUrl, resource.FetchOptions{})
ipv6Interfaces, err := findInterfacesWithIPv6()
if err != nil {
f.Logger.Info("No active IPv6 network interface found: %v", err)
// Fall back to IPv4 only
return fetchConfigFromMetadataServiceIPv4Only(f)
}

urls := []url.URL{userdataURLs[resource.IPv4]}

for _, ifaceName := range ipv6Interfaces {
ipv6Url := userdataURLs[resource.IPv6]
ipv6Url.Host = strings.Replace(ipv6Url.Host, "iface", ifaceName, 1)
urls = append(urls, ipv6Url)
}

// Use parallel fetching for all interfaces
cfg, _, err := fetchConfigParallel(f, urls)

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

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

func fetchConfigFromMetadataServiceIPv4Only(f *resource.Fetcher) ([]byte, error) {
urls := map[string]url.URL{
string(resource.IPv4): userdataURLs[resource.IPv4],
}

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

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

func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) (types.Config, report.Report, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

var (
err error
nbErrors int
)

cfg := make(map[url.URL][]byte)

success := make(chan url.URL, 1)
errors := make(chan error, len(urls))

// Use waitgroup to wait for all goroutines to complete
var wg sync.WaitGroup

fetch := func(_ context.Context, u url.URL) {
defer wg.Done()
d, e := f.FetchToBuffer(u, resource.FetchOptions{})
if e != nil {
f.Logger.Err("fetching configuration for %s: %v", u.String(), e)
err = e
errors <- e
} else {
cfg[u] = d
select {
case success <- u:
default:
}
}
}

// Start goroutines for all URLs
for _, u := range urls {
wg.Add(1)
go fetch(ctx, u)
}

// Wait for the first success or all failures
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()

select {
case u := <-success:
f.Logger.Debug("got configuration from: %s", u.String())
return util.ParseConfig(f.Logger, cfg[u])
case <-errors:
nbErrors++
if nbErrors == len(urls) {
f.Logger.Debug("all routines have failed to fetch configuration, returning last known error: %v", err)
return types.Config{}, report.Report{}, err
}
case <-done:
// All goroutines completed, check if we have any success
if len(cfg) > 0 {
// Return the first successful configuration
for u, data := range cfg {
f.Logger.Debug("got configuration from: %s", u.String())
return util.ParseConfig(f.Logger, data)
}
}
}

return types.Config{}, report.Report{}, err
}

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

var ipv6Interfaces []string
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 {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To16() != nil && ipnet.IP.To4() == nil {
ipv6Interfaces = append(ipv6Interfaces, iface.Name)
break
}
}
}

if len(ipv6Interfaces) == 0 {
return nil, fmt.Errorf("no active IPv6 network interface found")
}

return ipv6Interfaces, nil
}
99 changes: 98 additions & 1 deletion internal/resource/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import (
"io"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
"sync"
"syscall"
"time"

Expand All @@ -36,9 +38,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-v2/aws"
Expand All @@ -49,6 +55,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 @@ -325,10 +336,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 @@ -735,3 +753,82 @@ 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
mu sync.Mutex
)

cfg := make(map[url.URL][]byte)

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)
mu.Lock()
err = e
mu.Unlock()
errors <- e
return
}
_, _, parseErr := providersUtil.ParseConfig(f.Logger, d)
if parseErr != nil {
f.Logger.Err("parsing configuration from %s: %v", ip.String(), parseErr)
mu.Lock()
err = parseErr
mu.Unlock()
errors <- parseErr
return
}

mu.Lock()
cfg[ip] = d
mu.Unlock()
select {
case success <- ip:
default:
}
}

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

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

for {
select {
case ip := <-success:
f.Logger.Debug("got configuration from: %s", ip.String())
mu.Lock()
data := cfg[ip]
mu.Unlock()
return providersUtil.ParseConfig(f.Logger, data)
case <-errors:
nbErrors++
if nbErrors >= numGoroutines {
mu.Lock()
lastErr := err
mu.Unlock()
f.Logger.Debug("all routines have failed to fetch configuration, returning last known error: %v", lastErr)
return types.Config{}, report.Report{}, lastErr
}
}
}
}
Loading
Loading