Skip to content

Commit 9718d73

Browse files
mcanevetsmira
authored andcommitted
feat(machined): add IPv6 alias address support for OpenNebula (ETH*_ALIAS*_IP6)
Extends parseAliases to read ETH*_ALIAS*_IP6 (legacy: ETH*_ALIAS*_IPV6) and ETH*_ALIAS*_IP6_PREFIX_LENGTH (default 64), emitting an IPv6 AddressSpecSpec subject to the same EXTERNAL/DETACH skip logic as IPv4 aliases. Error tests for IPv4/IPv6 addresses, aliases, and gateway are consolidated into a single TestParseErrors function to avoid duplication. Signed-off-by: Mickaël Canévet <mickael.canevet@proton.ch> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com> (cherry picked from commit 4d0244d)
1 parent b649fb4 commit 9718d73

File tree

3 files changed

+249
-52
lines changed

3 files changed

+249
-52
lines changed

internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula/aliases_test.go

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ func TestParseAliases(t *testing.T) {
5454
ConfigLayer: network.ConfigPlatform,
5555
}
5656

57+
alias0IPv6 := network.AddressSpecSpec{
58+
Address: netip.MustParsePrefix("2001:db8::100/64"),
59+
LinkName: "eth0",
60+
Family: nethelpers.FamilyInet6,
61+
Scope: nethelpers.ScopeGlobal,
62+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
63+
ConfigLayer: network.ConfigPlatform,
64+
}
65+
5766
for _, tc := range []struct {
5867
name string
5968
extra string
@@ -133,6 +142,123 @@ ETH0_ALIAS0_DETACH = ""`,
133142
extra: "",
134143
wantAliasAddr: nil,
135144
},
145+
{
146+
name: "IPv6 alias included",
147+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
148+
ETH0_ALIAS0_IP6 = "2001:db8::100"
149+
ETH0_ALIAS0_EXTERNAL = "NO"
150+
ETH0_ALIAS0_DETACH = ""`,
151+
wantAliasAddr: []network.AddressSpecSpec{alias0IPv6},
152+
},
153+
{
154+
name: "ETH*_ALIAS*_IPV6 legacy alias used when IP6 absent",
155+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
156+
ETH0_ALIAS0_IPV6 = "2001:db8::100"
157+
ETH0_ALIAS0_EXTERNAL = "NO"
158+
ETH0_ALIAS0_DETACH = ""`,
159+
wantAliasAddr: []network.AddressSpecSpec{alias0IPv6},
160+
},
161+
{
162+
name: "IPv6 alias explicit prefix length respected",
163+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
164+
ETH0_ALIAS0_IP6 = "2001:db8::100"
165+
ETH0_ALIAS0_IP6_PREFIX_LENGTH = "48"
166+
ETH0_ALIAS0_EXTERNAL = "NO"
167+
ETH0_ALIAS0_DETACH = ""`,
168+
wantAliasAddr: []network.AddressSpecSpec{
169+
{
170+
Address: netip.MustParsePrefix("2001:db8::100/48"),
171+
LinkName: "eth0",
172+
Family: nethelpers.FamilyInet6,
173+
Scope: nethelpers.ScopeGlobal,
174+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
175+
ConfigLayer: network.ConfigPlatform,
176+
},
177+
},
178+
},
179+
{
180+
name: "EXTERNAL=YES skips IPv6 alias",
181+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
182+
ETH0_ALIAS0_IP6 = "2001:db8::100"
183+
ETH0_ALIAS0_EXTERNAL = "YES"
184+
ETH0_ALIAS0_DETACH = ""`,
185+
wantAliasAddr: nil,
186+
},
187+
{
188+
name: "DETACH non-empty skips IPv6 alias",
189+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
190+
ETH0_ALIAS0_IP6 = "2001:db8::100"
191+
ETH0_ALIAS0_EXTERNAL = "NO"
192+
ETH0_ALIAS0_DETACH = "yes"`,
193+
wantAliasAddr: nil,
194+
},
195+
{
196+
name: "mixed IPv4 and IPv6 aliases both emitted",
197+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
198+
ETH0_ALIAS0_IP = "192.168.1.100"
199+
ETH0_ALIAS0_MASK = "255.255.255.0"
200+
ETH0_ALIAS0_IP6 = "2001:db8::100"
201+
ETH0_ALIAS0_EXTERNAL = "NO"
202+
ETH0_ALIAS0_DETACH = ""`,
203+
wantAliasAddr: []network.AddressSpecSpec{alias0IPv4, alias0IPv6},
204+
},
205+
{
206+
name: "IPv6 ULA alias emits two IPv6 addresses",
207+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
208+
ETH0_ALIAS0_IP6 = "2001:db8::100"
209+
ETH0_ALIAS0_IP6_ULA = "fd00::100"
210+
ETH0_ALIAS0_EXTERNAL = "NO"
211+
ETH0_ALIAS0_DETACH = ""`,
212+
wantAliasAddr: []network.AddressSpecSpec{
213+
alias0IPv6,
214+
{
215+
Address: netip.MustParsePrefix("fd00::100/64"),
216+
LinkName: "eth0",
217+
Family: nethelpers.FamilyInet6,
218+
Scope: nethelpers.ScopeGlobal,
219+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
220+
ConfigLayer: network.ConfigPlatform,
221+
},
222+
},
223+
},
224+
{
225+
name: "EXTERNAL=YES skips IPv6 ULA alias",
226+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
227+
ETH0_ALIAS0_IP6_ULA = "fd00::100"
228+
ETH0_ALIAS0_EXTERNAL = "YES"
229+
ETH0_ALIAS0_DETACH = ""`,
230+
wantAliasAddr: nil,
231+
},
232+
{
233+
name: "DETACH non-empty skips IPv6 ULA alias",
234+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
235+
ETH0_ALIAS0_IP6_ULA = "fd00::100"
236+
ETH0_ALIAS0_EXTERNAL = "NO"
237+
ETH0_ALIAS0_DETACH = "yes"`,
238+
wantAliasAddr: nil,
239+
},
240+
{
241+
name: "mixed IPv4 and IPv6 and ULA alias emits all three addresses",
242+
extra: `ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
243+
ETH0_ALIAS0_IP = "192.168.1.100"
244+
ETH0_ALIAS0_MASK = "255.255.255.0"
245+
ETH0_ALIAS0_IP6 = "2001:db8::100"
246+
ETH0_ALIAS0_IP6_ULA = "fd00::100"
247+
ETH0_ALIAS0_EXTERNAL = "NO"
248+
ETH0_ALIAS0_DETACH = ""`,
249+
wantAliasAddr: []network.AddressSpecSpec{
250+
alias0IPv4,
251+
alias0IPv6,
252+
{
253+
Address: netip.MustParsePrefix("fd00::100/64"),
254+
LinkName: "eth0",
255+
Family: nethelpers.FamilyInet6,
256+
Scope: nethelpers.ScopeGlobal,
257+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
258+
ConfigLayer: network.ConfigPlatform,
259+
},
260+
},
261+
},
136262
} {
137263
t.Run(tc.name, func(t *testing.T) {
138264
t.Parallel()
@@ -151,13 +277,13 @@ ETH0_ALIAS0_DETACH = ""`,
151277
}
152278
}
153279

154-
func TestParseAliasErrors(t *testing.T) {
280+
func TestParseErrors(t *testing.T) {
155281
t.Parallel()
156282

157283
o := &opennebula.OpenNebula{}
158284
st := state.WrapCore(namespaced.NewState(inmem.Build))
159285

160-
t.Run("malformed IPv4 returns descriptive error", func(t *testing.T) {
286+
t.Run("malformed alias IPv4 returns descriptive error", func(t *testing.T) {
161287
t.Parallel()
162288

163289
ctx := aliasContext(`ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
@@ -170,4 +296,50 @@ ETH0_ALIAS0_DETACH = ""`)
170296
require.ErrorContains(t, err, "ETH0_ALIAS0")
171297
require.ErrorContains(t, err, "IPv4")
172298
})
299+
300+
t.Run("malformed alias IPv6 returns descriptive error", func(t *testing.T) {
301+
t.Parallel()
302+
303+
ctx := aliasContext(`ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
304+
ETH0_ALIAS0_IP6 = "notanip"
305+
ETH0_ALIAS0_EXTERNAL = "NO"
306+
ETH0_ALIAS0_DETACH = ""`)
307+
308+
_, err := o.ParseMetadata(st, ctx)
309+
require.ErrorContains(t, err, "ETH0_ALIAS0")
310+
require.ErrorContains(t, err, "IPv6")
311+
})
312+
313+
t.Run("malformed alias IPv6 ULA returns error containing alias name and ULA", func(t *testing.T) {
314+
t.Parallel()
315+
316+
ctx := aliasContext(`ETH0_ALIAS0_MAC = "02:00:c0:a8:01:64"
317+
ETH0_ALIAS0_IP6_ULA = "notanip"
318+
ETH0_ALIAS0_EXTERNAL = "NO"
319+
ETH0_ALIAS0_DETACH = ""`)
320+
321+
_, err := o.ParseMetadata(st, ctx)
322+
require.ErrorContains(t, err, "ETH0_ALIAS0")
323+
require.ErrorContains(t, err, "ULA")
324+
})
325+
326+
t.Run("malformed interface IPv6 address returns descriptive error", func(t *testing.T) {
327+
t.Parallel()
328+
329+
ctx := aliasContext("ETH0_IP6 = \"notanip\"")
330+
331+
_, err := o.ParseMetadata(st, ctx)
332+
require.ErrorContains(t, err, "ETH0")
333+
require.ErrorContains(t, err, "IPv6")
334+
})
335+
336+
t.Run("malformed IPv6 gateway returns descriptive error", func(t *testing.T) {
337+
t.Parallel()
338+
339+
ctx := aliasContext("ETH0_IP6 = \"2001:db8::1\"\nETH0_IP6_GATEWAY = \"notanip\"")
340+
341+
_, err := o.ParseMetadata(st, ctx)
342+
require.ErrorContains(t, err, "ETH0")
343+
require.ErrorContains(t, err, "gateway")
344+
})
173345
}

internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula/ipv6_test.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -183,30 +183,3 @@ func TestParseIPv6(t *testing.T) {
183183
})
184184
}
185185
}
186-
187-
func TestParseIPv6Errors(t *testing.T) {
188-
t.Parallel()
189-
190-
o := &opennebula.OpenNebula{}
191-
st := state.WrapCore(namespaced.NewState(inmem.Build))
192-
193-
t.Run("malformed IPv6 address returns descriptive error", func(t *testing.T) {
194-
t.Parallel()
195-
196-
ctx := ipv6Context("ETH0_IP6 = \"notanip\"")
197-
198-
_, err := o.ParseMetadata(st, ctx)
199-
require.ErrorContains(t, err, "ETH0")
200-
require.ErrorContains(t, err, "IPv6")
201-
})
202-
203-
t.Run("malformed IPv6 gateway returns descriptive error", func(t *testing.T) {
204-
t.Parallel()
205-
206-
ctx := ipv6Context("ETH0_IP6 = \"2001:db8::1\"\nETH0_IP6_GATEWAY = \"notanip\"")
207-
208-
_, err := o.ParseMetadata(st, ctx)
209-
require.ErrorContains(t, err, "ETH0")
210-
require.ErrorContains(t, err, "gateway")
211-
})
212-
}

internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula/opennebula.go

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,77 @@ func collectAliasNames(oneContext map[string]string, aliasPrefix string) []strin
8080
return aliasNames
8181
}
8282

83+
// parseAlias parses the addresses for a single alias entry. Returns nil, nil
84+
// when the alias should be skipped (DETACH non-empty or EXTERNAL=YES).
85+
func parseAlias(oneContext map[string]string, aliasName, ifaceNameLower string) ([]network.AddressSpecSpec, error) {
86+
// Skip detached aliases — reference: [ -z "${detach}" ]
87+
if oneContext[aliasName+"_DETACH"] != "" {
88+
return nil, nil
89+
}
90+
91+
// Skip externally managed aliases — reference: ! is_true "${external}"
92+
if strings.EqualFold(oneContext[aliasName+"_EXTERNAL"], "yes") {
93+
return nil, nil
94+
}
95+
96+
var addrs []network.AddressSpecSpec
97+
98+
if ipStr := oneContext[aliasName+"_IP"]; ipStr != "" {
99+
ipPrefix, err := address.IPPrefixFrom(ipStr, oneContext[aliasName+"_MASK"])
100+
if err != nil {
101+
return nil, fmt.Errorf("alias %s: failed to parse IPv4: %w", aliasName, err)
102+
}
103+
104+
addrs = append(addrs, network.AddressSpecSpec{
105+
Address: ipPrefix,
106+
LinkName: ifaceNameLower,
107+
Family: nethelpers.FamilyInet4,
108+
Scope: nethelpers.ScopeGlobal,
109+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
110+
ConfigLayer: network.ConfigPlatform,
111+
})
112+
}
113+
114+
ip6Str := oneContext[aliasName+"_IP6"]
115+
if ip6Str == "" {
116+
ip6Str = oneContext[aliasName+"_IPV6"]
117+
}
118+
119+
if ip6Str != "" {
120+
ip6Prefix, err := ip6PrefixFrom(ip6Str, oneContext[aliasName+"_IP6_PREFIX_LENGTH"])
121+
if err != nil {
122+
return nil, fmt.Errorf("alias %s: failed to parse IPv6: %w", aliasName, err)
123+
}
124+
125+
addrs = append(addrs, network.AddressSpecSpec{
126+
Address: ip6Prefix,
127+
LinkName: ifaceNameLower,
128+
Family: nethelpers.FamilyInet6,
129+
Scope: nethelpers.ScopeGlobal,
130+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
131+
ConfigLayer: network.ConfigPlatform,
132+
})
133+
}
134+
135+
if ulaStr := oneContext[aliasName+"_IP6_ULA"]; ulaStr != "" {
136+
ulaPrefix, err := ip6PrefixFrom(ulaStr, "64")
137+
if err != nil {
138+
return nil, fmt.Errorf("alias %s: failed to parse IPv6 ULA: %w", aliasName, err)
139+
}
140+
141+
addrs = append(addrs, network.AddressSpecSpec{
142+
Address: ulaPrefix,
143+
LinkName: ifaceNameLower,
144+
Family: nethelpers.FamilyInet6,
145+
Scope: nethelpers.ScopeGlobal,
146+
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
147+
ConfigLayer: network.ConfigPlatform,
148+
})
149+
}
150+
151+
return addrs, nil
152+
}
153+
83154
// parseAliases collects ETHn_ALIASm_* address entries for a given interface.
84155
// An alias is skipped when DETACH is non-empty OR EXTERNAL=YES, matching the
85156
// reference netcfg-networkd behavior (lines 395-400).
@@ -89,31 +160,12 @@ func parseAliases(oneContext map[string]string, ifaceName, ifaceNameLower string
89160
var addrs []network.AddressSpecSpec
90161

91162
for _, aliasName := range aliasNames {
92-
// Skip detached aliases — reference: [ -z "${detach}" ]
93-
if oneContext[aliasName+"_DETACH"] != "" {
94-
continue
95-
}
96-
97-
// Skip externally managed aliases — reference: ! is_true "${external}"
98-
if strings.EqualFold(oneContext[aliasName+"_EXTERNAL"], "yes") {
99-
continue
163+
aliasAddrs, err := parseAlias(oneContext, aliasName, ifaceNameLower)
164+
if err != nil {
165+
return nil, err
100166
}
101167

102-
if ipStr := oneContext[aliasName+"_IP"]; ipStr != "" {
103-
ipPrefix, err := address.IPPrefixFrom(ipStr, oneContext[aliasName+"_MASK"])
104-
if err != nil {
105-
return nil, fmt.Errorf("alias %s: failed to parse IPv4: %w", aliasName, err)
106-
}
107-
108-
addrs = append(addrs, network.AddressSpecSpec{
109-
Address: ipPrefix,
110-
LinkName: ifaceNameLower,
111-
Family: nethelpers.FamilyInet4,
112-
Scope: nethelpers.ScopeGlobal,
113-
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
114-
ConfigLayer: network.ConfigPlatform,
115-
})
116-
}
168+
addrs = append(addrs, aliasAddrs...)
117169
}
118170

119171
return addrs, nil

0 commit comments

Comments
 (0)