Skip to content

Commit 6b8a11e

Browse files
committed
Add single-stack IPv6 support
1 parent 1117566 commit 6b8a11e

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

docs/release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ nav_order: 9
1111
### Features
1212

1313
- Support partitioning disk with mounted partitions
14+
- Support IPv6 for single-stack OpenStack
1415

1516
### Changes
1617

internal/providers/openstack/openstack.go

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package openstack
2222
import (
2323
"context"
2424
"fmt"
25+
"net"
2526
"net/url"
2627
"os"
2728
"os/exec"
@@ -43,14 +44,6 @@ const (
4344
configDriveUserdataPath = "/openstack/latest/user_data"
4445
)
4546

46-
var (
47-
metadataServiceUrl = url.URL{
48-
Scheme: "http",
49-
Host: "169.254.169.254",
50-
Path: "openstack/latest/user_data",
51-
}
52-
)
53-
5447
func init() {
5548
platform.Register(platform.Provider{
5649
Name: "openstack",
@@ -166,14 +159,86 @@ func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string)
166159
return os.ReadFile(filepath.Join(mnt, configDriveUserdataPath))
167160
}
168161

162+
func FindIPv6InterfaceName() (string, error) {
163+
interfaces, err := net.Interfaces()
164+
if err != nil {
165+
return "", err
166+
}
167+
168+
for _, iface := range interfaces {
169+
// Check if the interface is up and not loopback
170+
if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback == 0 {
171+
addrs, err := iface.Addrs()
172+
if err != nil {
173+
continue
174+
}
175+
176+
for _, addr := range addrs {
177+
// Check if the address is an IPv6
178+
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() == nil && !ipnet.IP.IsLinkLocalUnicast() {
179+
return iface.Name, nil
180+
}
181+
}
182+
}
183+
}
184+
185+
return "", fmt.Errorf("no IPv6 interface name found")
186+
}
187+
169188
func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) {
170-
res, err := f.FetchToBuffer(metadataServiceUrl, resource.FetchOptions{})
171189

172-
// the metadata server exists but doesn't contain any actual metadata,
173-
// assume that there is no config specified
174-
if err == resource.ErrNotFound {
175-
return nil, nil
190+
// Find IPv6 name
191+
iface, err := FindIPv6InterfaceName()
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
// Construct URL
197+
var (
198+
ipv4MetadataServiceUrl = url.URL{
199+
Scheme: "http",
200+
Host: "169.254.169.254",
201+
Path: "openstack/latest/user_data",
202+
}
203+
ipv6MetadataServiceUrl = url.URL{
204+
Scheme: "http",
205+
Host: fmt.Sprintf("[fe80::a9fe:a9fe%%%s]", url.PathEscape(iface)),
206+
Path: "openstack/latest/user_data",
207+
}
208+
)
209+
210+
var resIPv4, resIPv6 []byte
211+
var errIPv4, errIPv6 error
212+
213+
// Try IPv4 endpoint
214+
resIPv4, errIPv4 = f.FetchToBuffer(ipv4MetadataServiceUrl, resource.FetchOptions{})
215+
if errIPv4 != nil && errIPv4 != resource.ErrNotFound {
216+
f.Logger.Err("Failed to fetch config from IPv4: %v", errIPv4)
217+
}
218+
219+
// Try IPv6 endpoint
220+
resIPv6, errIPv6 = f.FetchToBuffer(ipv6MetadataServiceUrl, resource.FetchOptions{})
221+
if errIPv6 != nil && errIPv6 != resource.ErrNotFound {
222+
f.Logger.Err("Failed to fetch config from IPv6: %v", errIPv6)
223+
}
224+
225+
// If both IPv4 and IPv6 have valid data, combine them
226+
if resIPv4 != nil && resIPv6 != nil {
227+
return append(resIPv4, resIPv6...), nil
228+
} else if resIPv4 != nil {
229+
return resIPv4, nil
230+
} else if resIPv6 != nil {
231+
return resIPv6, nil
232+
}
233+
234+
// If both endpoints fail, return the appropriate error
235+
if errIPv4 != nil {
236+
return nil, errIPv4
237+
}
238+
if errIPv6 != nil {
239+
return nil, errIPv6
176240
}
177241

178-
return res, err
242+
// If both endpoints return ErrNotFound
243+
return nil, nil
179244
}

0 commit comments

Comments
 (0)