@@ -21,11 +21,15 @@ package openstack
2121
2222import (
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
4650var (
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
173185func 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}
0 commit comments