Skip to content

Commit 3888f93

Browse files
authored
Merge pull request #1909 from yasminvalim/openstack-support
OpenStack single-stack IPv6 support
2 parents 920b0e8 + e3836ed commit 3888f93

4 files changed

Lines changed: 401 additions & 7 deletions

File tree

docs/release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Starting with this release, ignition-validate binaries are signed with the
9696
- Support partitioning disk with mounted partitions
9797
- Support Proxmox VE
9898
- Support gzipped Akamai user_data
99+
- Support IPv6 for single-stack OpenStack
99100

100101
### Changes
101102

internal/providers/openstack/openstack.go

Lines changed: 166 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ package openstack
2121

2222
import (
2323
"context"
24+
"encoding/json"
2425
"fmt"
26+
"net"
2527
"net/url"
2628
"os"
2729
"os/exec"
2830
"path/filepath"
31+
"strings"
32+
"sync"
2933
"time"
3034

3135
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
@@ -44,10 +48,18 @@ const (
4448
)
4549

4650
var (
47-
metadataServiceUrl = url.URL{
48-
Scheme: "http",
49-
Host: "169.254.169.254",
50-
Path: "openstack/latest/user_data",
51+
userdataURLs = map[string]url.URL{
52+
resource.IPv4: {
53+
Scheme: "http",
54+
Host: "169.254.169.254",
55+
Path: "openstack/latest/user_data",
56+
},
57+
58+
resource.IPv6: {
59+
Scheme: "http",
60+
Host: "[fe80::a9fe:a9fe%iface]",
61+
Path: "openstack/latest/user_data",
62+
},
5163
}
5264
)
5365

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

173185
func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) {
174-
res, err := f.FetchToBuffer(metadataServiceUrl, resource.FetchOptions{})
186+
ipv6Interfaces, err := findInterfacesWithIPv6()
187+
if err != nil {
188+
f.Logger.Info("No active IPv6 network interface found: %v", err)
189+
// Fall back to IPv4 only
190+
return fetchConfigFromMetadataServiceIPv4Only(f)
191+
}
192+
193+
urls := []url.URL{userdataURLs[resource.IPv4]}
194+
195+
for _, ifaceName := range ipv6Interfaces {
196+
ipv6Url := userdataURLs[resource.IPv6]
197+
ipv6Url.Host = strings.Replace(ipv6Url.Host, "iface", ifaceName, 1)
198+
urls = append(urls, ipv6Url)
199+
}
200+
201+
// Use parallel fetching for all interfaces
202+
cfg, _, err := fetchConfigParallel(f, urls)
203+
204+
// the metadata server exists but doesn't contain any actual metadata,
205+
// assume that there is no config specified
206+
if err == resource.ErrNotFound {
207+
return nil, nil
208+
}
209+
210+
data, err := json.Marshal(cfg)
211+
if err != nil {
212+
return nil, err
213+
}
214+
return data, nil
215+
}
216+
217+
func fetchConfigFromMetadataServiceIPv4Only(f *resource.Fetcher) ([]byte, error) {
218+
urls := map[string]url.URL{
219+
string(resource.IPv4): userdataURLs[resource.IPv4],
220+
}
221+
222+
cfg, _, err := resource.FetchConfigDualStack(
223+
f,
224+
urls,
225+
func(f *resource.Fetcher, u url.URL) ([]byte, error) {
226+
return f.FetchToBuffer(u, resource.FetchOptions{})
227+
},
228+
)
175229

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

182-
return res, err
236+
data, err := json.Marshal(cfg)
237+
if err != nil {
238+
return nil, err
239+
}
240+
return data, nil
241+
}
242+
243+
func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) (types.Config, report.Report, error) {
244+
ctx, cancel := context.WithCancel(context.Background())
245+
defer cancel()
246+
247+
var (
248+
err error
249+
nbErrors int
250+
)
251+
252+
cfg := make(map[url.URL][]byte)
253+
254+
success := make(chan url.URL, 1)
255+
errors := make(chan error, len(urls))
256+
257+
// Use waitgroup to wait for all goroutines to complete
258+
var wg sync.WaitGroup
259+
260+
fetch := func(_ context.Context, u url.URL) {
261+
defer wg.Done()
262+
d, e := f.FetchToBuffer(u, resource.FetchOptions{})
263+
if e != nil {
264+
f.Logger.Err("fetching configuration for %s: %v", u.String(), e)
265+
err = e
266+
errors <- e
267+
} else {
268+
cfg[u] = d
269+
select {
270+
case success <- u:
271+
default:
272+
}
273+
}
274+
}
275+
276+
// Start goroutines for all URLs
277+
for _, u := range urls {
278+
wg.Add(1)
279+
go fetch(ctx, u)
280+
}
281+
282+
// Wait for the first success or all failures
283+
done := make(chan struct{})
284+
go func() {
285+
wg.Wait()
286+
close(done)
287+
}()
288+
289+
select {
290+
case u := <-success:
291+
f.Logger.Debug("got configuration from: %s", u.String())
292+
return util.ParseConfig(f.Logger, cfg[u])
293+
case <-errors:
294+
nbErrors++
295+
if nbErrors == len(urls) {
296+
f.Logger.Debug("all routines have failed to fetch configuration, returning last known error: %v", err)
297+
return types.Config{}, report.Report{}, err
298+
}
299+
case <-done:
300+
// All goroutines completed, check if we have any success
301+
if len(cfg) > 0 {
302+
// Return the first successful configuration
303+
for u, data := range cfg {
304+
f.Logger.Debug("got configuration from: %s", u.String())
305+
return util.ParseConfig(f.Logger, data)
306+
}
307+
}
308+
}
309+
310+
return types.Config{}, report.Report{}, err
311+
}
312+
313+
func findInterfacesWithIPv6() ([]string, error) {
314+
interfaces, err := net.Interfaces()
315+
if err != nil {
316+
return nil, fmt.Errorf("error fetching network interfaces: %v", err)
317+
}
318+
319+
var ipv6Interfaces []string
320+
for _, iface := range interfaces {
321+
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
322+
continue
323+
}
324+
325+
addrs, err := iface.Addrs()
326+
if err != nil {
327+
continue
328+
}
329+
330+
for _, addr := range addrs {
331+
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To16() != nil && ipnet.IP.To4() == nil {
332+
ipv6Interfaces = append(ipv6Interfaces, iface.Name)
333+
break
334+
}
335+
}
336+
}
337+
338+
if len(ipv6Interfaces) == 0 {
339+
return nil, fmt.Errorf("no active IPv6 network interface found")
340+
}
341+
342+
return ipv6Interfaces, nil
183343
}

internal/resource/url.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import (
2525
"io"
2626
"net"
2727
"net/http"
28+
"net/netip"
2829
"net/url"
2930
"os"
3031
"strings"
32+
"sync"
3133
"syscall"
3234
"time"
3335

@@ -36,9 +38,13 @@ import (
3638
configErrors "github.com/coreos/ignition/v2/config/shared/errors"
3739
"github.com/coreos/ignition/v2/internal/log"
3840
"github.com/coreos/ignition/v2/internal/util"
41+
"github.com/coreos/vcontext/report"
3942
"golang.org/x/oauth2/google"
4043
"google.golang.org/api/option"
4144

45+
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
46+
providersUtil "github.com/coreos/ignition/v2/internal/providers/util"
47+
4248
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
4349
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
4450
"github.com/aws/aws-sdk-go-v2/aws"
@@ -49,6 +55,11 @@ import (
4955
"github.com/vincent-petithory/dataurl"
5056
)
5157

58+
const (
59+
IPv4 = "ipv4"
60+
IPv6 = "ipv6"
61+
)
62+
5263
var (
5364
ErrSchemeUnsupported = errors.New("unsupported source scheme")
5465
ErrPathNotAbsolute = errors.New("path is not absolute")
@@ -325,10 +336,17 @@ func (f *Fetcher) fetchFromHTTP(u url.URL, dest io.Writer, opts FetchOptions) er
325336
p int
326337
)
327338

339+
host := u.Hostname()
340+
addr, _ := netip.ParseAddr(host)
341+
network := "tcp6"
342+
if addr.Is4() {
343+
network = "tcp4"
344+
}
345+
328346
// Assert that the port is not already used.
329347
for {
330348
p = opts.LocalPort()
331-
l, err := net.Listen("tcp4", fmt.Sprintf(":%d", p))
349+
l, err := net.Listen(network, fmt.Sprintf(":%d", p))
332350
if err != nil && errors.Is(err, syscall.EADDRINUSE) {
333351
continue
334352
} else if err == nil {
@@ -735,3 +753,82 @@ func (f *Fetcher) parseARN(arnURL string) (string, string, string, string, error
735753
key := strings.Join(urlSplit[1:], "/")
736754
return bucket, key, "", regionHint, nil
737755
}
756+
757+
// FetchConfigDualStack is a function that takes care of fetching Ignition configuration on systems where IPv4 only, IPv6 only or both are available.
758+
// 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
759+
// return the first configuration successfully fetched.
760+
func FetchConfigDualStack(f *Fetcher, userdataURLs map[string]url.URL, fetchConfig func(*Fetcher, url.URL) ([]byte, error)) (types.Config, report.Report, error) {
761+
ctx, cancel := context.WithCancel(context.Background())
762+
defer cancel()
763+
764+
var (
765+
err error
766+
nbErrors int
767+
mu sync.Mutex
768+
)
769+
770+
cfg := make(map[url.URL][]byte)
771+
772+
success := make(chan url.URL, 1)
773+
errors := make(chan error, 2)
774+
775+
fetch := func(ctx context.Context, ip url.URL) {
776+
d, e := fetchConfig(f, ip)
777+
if e != nil {
778+
f.Logger.Err("fetching configuration for %s: %v", ip.String(), e)
779+
mu.Lock()
780+
err = e
781+
mu.Unlock()
782+
errors <- e
783+
return
784+
}
785+
_, _, parseErr := providersUtil.ParseConfig(f.Logger, d)
786+
if parseErr != nil {
787+
f.Logger.Err("parsing configuration from %s: %v", ip.String(), parseErr)
788+
mu.Lock()
789+
err = parseErr
790+
mu.Unlock()
791+
errors <- parseErr
792+
return
793+
}
794+
795+
mu.Lock()
796+
cfg[ip] = d
797+
mu.Unlock()
798+
select {
799+
case success <- ip:
800+
default:
801+
}
802+
}
803+
804+
numGoroutines := 0
805+
if ipv4, ok := userdataURLs[IPv4]; ok {
806+
go fetch(ctx, ipv4)
807+
numGoroutines++
808+
}
809+
810+
if ipv6, ok := userdataURLs[IPv6]; ok {
811+
go fetch(ctx, ipv6)
812+
numGoroutines++
813+
}
814+
815+
for {
816+
select {
817+
case ip := <-success:
818+
f.Logger.Debug("got configuration from: %s", ip.String())
819+
mu.Lock()
820+
data := cfg[ip]
821+
mu.Unlock()
822+
return providersUtil.ParseConfig(f.Logger, data)
823+
case <-errors:
824+
nbErrors++
825+
if nbErrors >= numGoroutines {
826+
mu.Lock()
827+
lastErr := err
828+
mu.Unlock()
829+
f.Logger.Debug("all routines have failed to fetch configuration, returning last known error: %v", lastErr)
830+
return types.Config{}, report.Report{}, lastErr
831+
}
832+
}
833+
}
834+
}

0 commit comments

Comments
 (0)