Skip to content
Open
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 @@ -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
173 changes: 167 additions & 6 deletions internal/providers/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,23 @@

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"
"github.com/coreos/ignition/v2/internal/distro"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/platform"
"github.com/coreos/ignition/v2/internal/providers/util"

Check failure on line 39 in internal/providers/openstack/openstack.go

View workflow job for this annotation

GitHub Actions / Test (1.23.x)

ST1019: package "github.com/coreos/ignition/v2/internal/providers/util" is being imported more than once (staticcheck)
providersUtil "github.com/coreos/ignition/v2/internal/providers/util"

Check failure on line 40 in internal/providers/openstack/openstack.go

View workflow job for this annotation

GitHub Actions / Test (1.23.x)

ST1019(related information): other import of "github.com/coreos/ignition/v2/internal/providers/util" (staticcheck)
"github.com/coreos/ignition/v2/internal/resource"
ut "github.com/coreos/ignition/v2/internal/util"

Expand All @@ -44,10 +49,18 @@
)

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 +180,161 @@
}

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 {

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 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 providersUtil.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 providersUtil.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 {
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 {
ipv6Interfaces = append(ipv6Interfaces, iface.Name)
break
}
}
}

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

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