From b9f4f6e2fb01af1bdf2e87a1c49dc4cbbf53ed2f Mon Sep 17 00:00:00 2001 From: Yasmin Valim Date: Mon, 14 Apr 2025 12:21:27 -0300 Subject: [PATCH 1/7] release-notes: add new feature entry --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bbdd85257..4f7644a5d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -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 From 9ad66ad9a9926ada3f06a28d7446826d29a3053e Mon Sep 17 00:00:00 2001 From: Mathieu Tortuyaux Date: Fri, 11 Apr 2025 16:14:33 +0200 Subject: [PATCH 2/7] url: try local port on both IP stacks Signed-off-by: Mathieu Tortuyaux --- internal/resource/url.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/resource/url.go b/internal/resource/url.go index 4d7a895df..695b7ea89 100644 --- a/internal/resource/url.go +++ b/internal/resource/url.go @@ -25,6 +25,7 @@ import ( "io" "net" "net/http" + "net/netip" "net/url" "os" "strings" @@ -330,10 +331,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 { From d5f444c335dadcb74e5cb719b753e5143cd31adf Mon Sep 17 00:00:00 2001 From: Mathieu Tortuyaux Date: Fri, 11 Apr 2025 16:05:31 +0200 Subject: [PATCH 3/7] url: support both IPv4 and IPv6 This defines a wrapper that will try in paralell both IPv4 and IPv6 when the provider declares those two IPs. Signed-off-by: Mathieu Tortuyaux --- internal/resource/url.go | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/internal/resource/url.go b/internal/resource/url.go index 695b7ea89..f91ef4255 100644 --- a/internal/resource/url.go +++ b/internal/resource/url.go @@ -37,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" @@ -53,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") @@ -733,3 +742,36 @@ 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. +func FetchConfigDualStack(f *Fetcher, userdataURLs map[string]url.URL, fetchConfig func(*Fetcher, url.URL) ([]byte, error)) (types.Config, report.Report, error) { + var ( + data []byte + err error + ) + success := make(chan string, 1) + + fetch := func(ip url.URL) { + data, err = fetchConfig(f, ip) + if err != nil { + f.Logger.Err("fetching configuration for %s: %v", ip.String(), err) + return + } + + success <- ip.String() + } + + if ipv4, ok := userdataURLs[IPv4]; ok { + go fetch(ipv4) + } + + if ipv6, ok := userdataURLs[IPv6]; ok { + go fetch(ipv6) + } + + // Wait for one success. (i.e wait for the first configuration to be available) + ip := <-success + f.Logger.Debug("got configuration from: %s", ip) + + return providersUtil.ParseConfig(f.Logger, data) +} From d030c0747963dc0d243bfaf6fbc08ff71216ce51 Mon Sep 17 00:00:00 2001 From: Yasmin Valim Date: Mon, 28 Apr 2025 11:46:26 -0300 Subject: [PATCH 4/7] openstack.go: add FetchConfigDualStackin openstack --- internal/providers/openstack/openstack.go | 71 +++++++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/internal/providers/openstack/openstack.go b/internal/providers/openstack/openstack.go index 3578256ea..041da93e0 100644 --- a/internal/providers/openstack/openstack.go +++ b/internal/providers/openstack/openstack.go @@ -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" @@ -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]", + Path: "openstack/latest/user_data", + }, } ) @@ -167,7 +178,26 @@ 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() + 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 @@ -175,5 +205,34 @@ func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) { return nil, nil } - return res, err + data, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + return data, nil +} + +func findInterfaceWithIPv6() (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 { + 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") } From aabc1dbecf416b4a36352a64f462c9d6aeea148c Mon Sep 17 00:00:00 2001 From: Mathieu Tortuyaux Date: Fri, 11 Apr 2025 16:05:31 +0200 Subject: [PATCH 5/7] url: support both IPv4 and IPv6 This defines a wrapper that will try in paralell both IPv4 and IPv6 when the provider declares those two IPs. Signed-off-by: Mathieu Tortuyaux --- internal/resource/url.go | 57 ++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/internal/resource/url.go b/internal/resource/url.go index f91ef4255..c2f4dd50f 100644 --- a/internal/resource/url.go +++ b/internal/resource/url.go @@ -744,34 +744,57 @@ func (f *Fetcher) parseARN(arnURL string) (string, string, string, string, error } // 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 ( - data []byte - err error + err error + nbErrors int ) - success := make(chan string, 1) - fetch := func(ip url.URL) { - data, err = fetchConfig(f, ip) - if err != nil { - f.Logger.Err("fetching configuration for %s: %v", ip.String(), err) - return - } + // cfg holds the configuration for a given IP + cfg := make(map[url.URL][]byte) - success <- ip.String() + // 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(ipv4) + go fetch(ctx, ipv4) } if ipv6, ok := userdataURLs[IPv6]; ok { - go fetch(ipv6) + 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 + } } - // Wait for one success. (i.e wait for the first configuration to be available) - ip := <-success - f.Logger.Debug("got configuration from: %s", ip) - - return providersUtil.ParseConfig(f.Logger, data) + // we should never reach this line + return types.Config{}, report.Report{}, err } From b6f610c5ad6121fc16484963da75b4ac9e02a44f Mon Sep 17 00:00:00 2001 From: yasminvalim Date: Tue, 14 Oct 2025 16:46:33 -0300 Subject: [PATCH 6/7] openstack: implement parallel IPv6 interface discovery with fan-out pattern using goroutines and waitgroups --- internal/providers/openstack/openstack.go | 124 ++++++++++++++++++++-- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/internal/providers/openstack/openstack.go b/internal/providers/openstack/openstack.go index 041da93e0..b3df28155 100644 --- a/internal/providers/openstack/openstack.go +++ b/internal/providers/openstack/openstack.go @@ -29,6 +29,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "time" "github.com/coreos/ignition/v2/config/v3_6_experimental/types" @@ -36,6 +37,7 @@ import ( "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/platform" "github.com/coreos/ignition/v2/internal/providers/util" + providersUtil "github.com/coreos/ignition/v2/internal/providers/util" "github.com/coreos/ignition/v2/internal/resource" ut "github.com/coreos/ignition/v2/internal/util" @@ -178,17 +180,40 @@ func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string) } func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) { - urls := map[string]url.URL{ - string(resource.IPv4): userdataURLs[resource.IPv4], + 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) } - ifaceName, err := findInterfaceWithIPv6() - if err == nil { + urls := []url.URL{userdataURLs[resource.IPv4]} + + for _, ifaceName := range ipv6Interfaces { 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) + 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( @@ -212,12 +237,83 @@ func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) { return data, nil } -func findInterfaceWithIPv6() (string, error) { +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 "", fmt.Errorf("error fetching network interfaces: %v", err) + 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 @@ -230,9 +326,15 @@ func findInterfaceWithIPv6() (string, error) { for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To16() != nil && ipnet.IP.To4() == nil { - return iface.Name, nil + ipv6Interfaces = append(ipv6Interfaces, iface.Name) + break } } } - return "", fmt.Errorf("no active IPv6 network interface found") + + if len(ipv6Interfaces) == 0 { + return nil, fmt.Errorf("no active IPv6 network interface found") + } + + return ipv6Interfaces, nil } From e7b3e2c2232e1cb255e8048f272d43a39633c0f2 Mon Sep 17 00:00:00 2001 From: yasminvalim Date: Thu, 16 Oct 2025 17:31:27 -0300 Subject: [PATCH 7/7] Add some debugging for IPv6 single-stack implementation --- internal/providers/openstack/openstack.go | 156 ++++++++++++++++++++-- 1 file changed, 144 insertions(+), 12 deletions(-) diff --git a/internal/providers/openstack/openstack.go b/internal/providers/openstack/openstack.go index b3df28155..d291adc0c 100644 --- a/internal/providers/openstack/openstack.go +++ b/internal/providers/openstack/openstack.go @@ -92,19 +92,27 @@ func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) { dispatch := func(name string, fn func() ([]byte, error)) { dispatchCount++ go func() { + f.Logger.Info("OpenStack provider: starting fetch attempt from %s", name) raw, err := fn() if err != nil { switch err { case context.Canceled: + f.Logger.Debug("OpenStack provider: fetch from %s was canceled (another source succeeded)", name) default: - f.Logger.Err("failed to fetch config from %s: %v", name, err) + f.Logger.Warning("OpenStack provider: failed to fetch config from %s: %v", name, err) } errChan <- err return } - data = raw - cancel() + if len(raw) > 0 { + f.Logger.Info("OpenStack provider: successfully fetched config from %s (%d bytes)", name, len(raw)) + data = raw + cancel() + } else { + f.Logger.Info("OpenStack provider: %s returned empty config", name) + errChan <- nil + } }() } @@ -124,16 +132,25 @@ Loop: for { select { case <-ctx.Done(): + f.Logger.Info("OpenStack provider: config fetch completed successfully") break Loop case <-errChan: dispatchCount-- if dispatchCount == 0 { - f.Logger.Info("couldn't fetch config") + if len(data) == 0 { + f.Logger.Warning("OpenStack provider: couldn't fetch config from any source (config drive or metadata service)") + } else { + f.Logger.Info("OpenStack provider: all fetch attempts completed") + } break Loop } } } + if len(data) == 0 { + f.Logger.Info("OpenStack provider: no config data available - this is normal if no userdata was provided") + } + return util.ParseConfig(f.Logger, data) } @@ -180,60 +197,107 @@ func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string) } func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) { + f.Logger.Info("OpenStack metadata service: starting fetch - checking network interfaces for IPv6 support") + + // Log network interface information for debugging + logNetworkInterfaces(f.Logger) + ipv6Interfaces, err := findInterfacesWithIPv6() if err != nil { - f.Logger.Info("No active IPv6 network interface found: %v", err) + f.Logger.Info("OpenStack metadata service: no active IPv6 network interface found: %v", err) + f.Logger.Info("OpenStack metadata service: falling back to IPv4-only metadata service fetch") // Fall back to IPv4 only return fetchConfigFromMetadataServiceIPv4Only(f) } - urls := []url.URL{userdataURLs[resource.IPv4]} + f.Logger.Info("OpenStack metadata service: found %d IPv6 interface(s): %v", len(ipv6Interfaces), ipv6Interfaces) + + ipv4Url := userdataURLs[resource.IPv4] + urls := []url.URL{ipv4Url} + f.Logger.Info("OpenStack metadata service: added IPv4 metadata URL: %s", ipv4Url.String()) for _, ifaceName := range ipv6Interfaces { ipv6Url := userdataURLs[resource.IPv6] ipv6Url.Host = strings.Replace(ipv6Url.Host, "iface", ifaceName, 1) urls = append(urls, ipv6Url) + f.Logger.Info("OpenStack metadata service: added IPv6 metadata URL for interface %s: %s", ifaceName, ipv6Url.String()) + } + + f.Logger.Info("OpenStack metadata service: total URLs to try: %d (1 IPv4 + %d IPv6)", len(urls), len(ipv6Interfaces)) + for i, u := range urls { + f.Logger.Info("OpenStack metadata service: URL %d: %s", i+1, u.String()) } // Use parallel fetching for all interfaces + f.Logger.Info("OpenStack metadata service: starting parallel fetch for all URLs") 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 { + f.Logger.Info("OpenStack metadata service: metadata server returned 404 (not found) - no config specified") return nil, nil } + if err != nil { + f.Logger.Err("OpenStack metadata service: error during parallel fetch: %v", err) + return nil, err + } + + f.Logger.Info("OpenStack metadata service: successfully fetched configuration, marshaling to JSON") data, err := json.Marshal(cfg) if err != nil { + f.Logger.Err("OpenStack metadata service: error marshaling config to JSON: %v", err) return nil, err } + + f.Logger.Info("OpenStack metadata service: successfully marshaled config, returning %d bytes", len(data)) return data, nil } func fetchConfigFromMetadataServiceIPv4Only(f *resource.Fetcher) ([]byte, error) { + ipv4Url := userdataURLs[resource.IPv4] + f.Logger.Info("OpenStack metadata service: attempting IPv4-only fetch at %s", ipv4Url.String()) + urls := map[string]url.URL{ - string(resource.IPv4): userdataURLs[resource.IPv4], + string(resource.IPv4): ipv4Url, } cfg, _, err := resource.FetchConfigDualStack( f, urls, func(f *resource.Fetcher, u url.URL) ([]byte, error) { - return f.FetchToBuffer(u, resource.FetchOptions{}) + f.Logger.Info("OpenStack metadata service: fetching from IPv4 URL: %s", u.String()) + data, fetchErr := f.FetchToBuffer(u, resource.FetchOptions{}) + if fetchErr == nil && len(data) > 0 { + f.Logger.Info("OpenStack metadata service: successfully fetched from IPv4 (%d bytes)", len(data)) + } else if fetchErr != nil { + f.Logger.Warning("OpenStack metadata service: IPv4 fetch failed: %v", fetchErr) + } + return data, fetchErr }, ) // the metadata server exists but doesn't contain any actual metadata, // assume that there is no config specified if err == resource.ErrNotFound { + f.Logger.Info("OpenStack metadata service: IPv4 metadata service returned 404 (not found) - no config specified") return nil, nil } + if err != nil { + f.Logger.Err("OpenStack metadata service: IPv4-only fetch error: %v", err) + return nil, err + } + + f.Logger.Info("OpenStack metadata service: marshaling IPv4 config to JSON") data, err := json.Marshal(cfg) if err != nil { + f.Logger.Err("OpenStack metadata service: error marshaling IPv4 config to JSON: %v", err) return nil, err } + + f.Logger.Info("OpenStack metadata service: successfully marshaled IPv4 config, returning %d bytes", len(data)) return data, nil } @@ -256,12 +320,22 @@ func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) (types.Config, rep fetch := func(_ context.Context, u url.URL) { defer wg.Done() + f.Logger.Info("OpenStack metadata service: attempting fetch from %s", u.String()) d, e := f.FetchToBuffer(u, resource.FetchOptions{}) if e != nil { - f.Logger.Err("fetching configuration for %s: %v", u.String(), e) + if e == resource.ErrNotFound { + f.Logger.Info("OpenStack metadata service: %s returned 404 (not found)", u.String()) + } else { + f.Logger.Warning("OpenStack metadata service: fetching configuration from %s failed: %v", u.String(), e) + } err = e errors <- e } else { + if len(d) > 0 { + f.Logger.Info("OpenStack metadata service: successfully fetched configuration from %s (%d bytes)", u.String(), len(d)) + } else { + f.Logger.Info("OpenStack metadata service: %s returned empty response", u.String()) + } cfg[u] = d select { case success <- u: @@ -285,12 +359,12 @@ func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) (types.Config, rep select { case u := <-success: - f.Logger.Debug("got configuration from: %s", u.String()) + f.Logger.Info("OpenStack metadata service: 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) + f.Logger.Warning("OpenStack metadata service: all fetch routines have failed, returning last known error: %v", err) return types.Config{}, report.Report{}, err } case <-done: @@ -298,7 +372,7 @@ func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) (types.Config, rep if len(cfg) > 0 { // Return the first successful configuration for u, data := range cfg { - f.Logger.Debug("got configuration from: %s", u.String()) + f.Logger.Info("OpenStack metadata service: got configuration from: %s (%d bytes)", u.String(), len(data)) return providersUtil.ParseConfig(f.Logger, data) } } @@ -307,6 +381,64 @@ func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) (types.Config, rep return types.Config{}, report.Report{}, err } +// logNetworkInterfaces logs available network interfaces for debugging IPv6 support +func logNetworkInterfaces(logger *log.Logger) { + logger.Info("OpenStack metadata service: checking network interfaces for IPv6 support") + + interfaces, err := net.Interfaces() + if err != nil { + logger.Warning("OpenStack metadata service: failed to enumerate network interfaces: %v", err) + return + } + + var hasIPv4, hasIPv6 bool + var ipv4Addrs, ipv6Addrs []string + var activeInterfaces []string + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + activeInterfaces = append(activeInterfaces, iface.Name) + + addrs, err := iface.Addrs() + if err != nil { + logger.Debug("OpenStack metadata service: failed to get addresses for interface %s: %v", iface.Name, err) + continue + } + + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err != nil { + continue + } + + if ip.To4() != nil { + hasIPv4 = true + if ip.IsLinkLocalUnicast() { + ipv4Addrs = append(ipv4Addrs, ip.String()) + } + } else if ip.To16() != nil && ip.To4() == nil { + hasIPv6 = true + if ip.IsLinkLocalUnicast() { + ipv6Addrs = append(ipv6Addrs, ip.String()) + } + } + } + } + + logger.Info("OpenStack metadata service: network interface status - IPv4: %v, IPv6: %v", hasIPv4, hasIPv6) + logger.Info("OpenStack metadata service: found %d active network interface(s): %v", len(activeInterfaces), activeInterfaces) + if len(ipv4Addrs) > 0 { + logger.Info("OpenStack metadata service: found IPv4 link-local addresses: %v", ipv4Addrs) + } + if len(ipv6Addrs) > 0 { + logger.Info("OpenStack metadata service: found IPv6 link-local addresses: %v", ipv6Addrs) + } else if hasIPv6 { + logger.Warning("OpenStack metadata service: IPv6 is available but no link-local addresses found") + } +} + func findInterfacesWithIPv6() ([]string, error) { interfaces, err := net.Interfaces() if err != nil {