Skip to content

Commit b649fb4

Browse files
mcanevetsmira
authored andcommitted
feat(machined): support ETH*_IP6_METHOD (static/dhcp/auto/disable) for OpenNebula
Dispatches on ETH*_IP6_METHOD before the static IPv6 path: - disable: skip all IPv6 config for the interface - auto: emit nothing; Talos accepts Router Advertisements by default so SLAAC address auto-configuration works without any explicit operator - dhcp: emit OperatorDHCP6 with RouteMetric from ETH*_IP6_METRIC (default 1) - static / empty: fall through to the existing static address path Signed-off-by: Mickaël Canévet <mickael.canevet@proton.ch> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com> (cherry picked from commit 5bb8962)
1 parent c81df6f commit b649fb4

File tree

3 files changed

+209
-13
lines changed

3 files changed

+209
-13
lines changed

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

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ func ipv6Context(extra string) []byte {
2929
return []byte(ipv6ContextBase + extra)
3030
}
3131

32-
func TestParseIPv6Static(t *testing.T) {
32+
func TestParseIPv6(t *testing.T) {
3333
t.Parallel()
3434

3535
o := &opennebula.OpenNebula{}
3636
st := state.WrapCore(namespaced.NewState(inmem.Build))
3737

38-
defaultGWRoute := func(gw string, priority uint32) network.RouteSpecSpec {
38+
gw6Route := func(gw string, priority uint32) network.RouteSpecSpec {
3939
return network.RouteSpecSpec{
4040
ConfigLayer: network.ConfigPlatform,
4141
Gateway: netip.MustParseAddr(gw),
@@ -49,11 +49,25 @@ func TestParseIPv6Static(t *testing.T) {
4949
}
5050
}
5151

52+
dhcp6Op := func(metric uint32) network.OperatorSpecSpec {
53+
return network.OperatorSpecSpec{
54+
Operator: network.OperatorDHCP6,
55+
LinkName: "eth0",
56+
RequireUp: true,
57+
DHCP6: network.DHCP6OperatorSpec{
58+
RouteMetric: metric,
59+
SkipHostnameRequest: true,
60+
},
61+
ConfigLayer: network.ConfigPlatform,
62+
}
63+
}
64+
5265
for _, tc := range []struct {
53-
name string
54-
extra string
55-
wantAddrs []netip.Prefix
56-
wantRoutes []network.RouteSpecSpec
66+
name string
67+
extra string
68+
wantAddrs []netip.Prefix
69+
wantRoutes []network.RouteSpecSpec
70+
wantOperators []network.OperatorSpecSpec
5771
}{
5872
{
5973
name: "static IPv6 address with explicit prefix length",
@@ -84,24 +98,52 @@ func TestParseIPv6Static(t *testing.T) {
8498
name: "IPv6 gateway emits default route with metric 1",
8599
extra: "ETH0_IP6 = \"2001:db8::1\"\nETH0_IP6_GATEWAY = \"2001:db8::fffe\"",
86100
wantAddrs: []netip.Prefix{netip.MustParsePrefix("2001:db8::1/64")},
87-
wantRoutes: []network.RouteSpecSpec{defaultGWRoute("2001:db8::fffe", 1)},
101+
wantRoutes: []network.RouteSpecSpec{gw6Route("2001:db8::fffe", 1)},
88102
},
89103
{
90104
name: "ETH*_GATEWAY6 legacy alias used when ETH*_IP6_GATEWAY absent",
91105
extra: "ETH0_IP6 = \"2001:db8::1\"\nETH0_GATEWAY6 = \"2001:db8::fffe\"",
92106
wantAddrs: []netip.Prefix{netip.MustParsePrefix("2001:db8::1/64")},
93-
wantRoutes: []network.RouteSpecSpec{defaultGWRoute("2001:db8::fffe", 1)},
107+
wantRoutes: []network.RouteSpecSpec{gw6Route("2001:db8::fffe", 1)},
94108
},
95109
{
96110
name: "ETH*_IP6_METRIC overrides default metric of 1",
97111
extra: "ETH0_IP6 = \"2001:db8::1\"\nETH0_IP6_GATEWAY = \"2001:db8::fffe\"\nETH0_IP6_METRIC = \"100\"",
98112
wantAddrs: []netip.Prefix{netip.MustParsePrefix("2001:db8::1/64")},
99-
wantRoutes: []network.RouteSpecSpec{defaultGWRoute("2001:db8::fffe", 100)},
113+
wantRoutes: []network.RouteSpecSpec{gw6Route("2001:db8::fffe", 100)},
100114
},
101115
{
102-
name: "no IPv6 variables — no IPv6 addresses or routes",
116+
name: "no IPv6 variables — no IPv6 output",
103117
extra: "",
104118
},
119+
{
120+
name: "IP6_METHOD=dhcp emits OperatorDHCP6 with default metric 1",
121+
extra: "ETH0_IP6_METHOD = \"dhcp\"",
122+
wantOperators: []network.OperatorSpecSpec{dhcp6Op(1)},
123+
},
124+
{
125+
name: "IP6_METHOD=dhcp with IP6_METRIC uses custom metric",
126+
extra: "ETH0_IP6_METHOD = \"dhcp\"\nETH0_IP6_METRIC = \"200\"",
127+
wantOperators: []network.OperatorSpecSpec{dhcp6Op(200)},
128+
},
129+
{
130+
name: "IP6_METHOD=auto emits nothing",
131+
extra: "ETH0_IP6_METHOD = \"auto\"\nETH0_IP6 = \"2001:db8::1\"",
132+
},
133+
{
134+
name: "IP6_METHOD=disable emits nothing even if IP6 is set",
135+
extra: "ETH0_IP6_METHOD = \"disable\"\nETH0_IP6 = \"2001:db8::1\"",
136+
},
137+
{
138+
name: "IP6_METHOD=static with IP6 set emits address",
139+
extra: "ETH0_IP6_METHOD = \"static\"\nETH0_IP6 = \"2001:db8::1\"",
140+
wantAddrs: []netip.Prefix{netip.MustParsePrefix("2001:db8::1/64")},
141+
},
142+
{
143+
name: "IP6_METHOD absent and IP6 set uses static path",
144+
extra: "ETH0_IP6 = \"2001:db8::1\"",
145+
wantAddrs: []netip.Prefix{netip.MustParsePrefix("2001:db8::1/64")},
146+
},
105147
} {
106148
t.Run(tc.name, func(t *testing.T) {
107149
t.Parallel()
@@ -128,6 +170,16 @@ func TestParseIPv6Static(t *testing.T) {
128170
}
129171

130172
assert.Equal(t, tc.wantRoutes, ip6Routes)
173+
174+
var ip6Operators []network.OperatorSpecSpec
175+
176+
for _, op := range networkConfig.Operators {
177+
if op.Operator == network.OperatorDHCP6 {
178+
ip6Operators = append(ip6Operators, op)
179+
}
180+
}
181+
182+
assert.Equal(t, tc.wantOperators, ip6Operators)
131183
})
132184
}
133185
}

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
3030
)
3131

32+
const methodSkip = "skip"
33+
3234
// OpenNebula is the concrete type that implements the runtime.Platform interface.
3335
type OpenNebula struct{}
3436

@@ -362,6 +364,10 @@ func parseIPv4StaticConfig(
362364
// parseInterfaceIPv4 configures the IPv4 stack for one interface.
363365
// Dispatches to DHCP4 operator or static config based on ETH*_METHOD.
364366
func parseInterfaceIPv4(oneContext map[string]string, ifaceName, ifaceNameLower string, networkConfig *runtime.PlatformNetworkConfig, allDNSIPs *[]netip.Addr, allSearchDomains *[]string) error {
367+
if oneContext[ifaceName+"_METHOD"] == methodSkip {
368+
return nil
369+
}
370+
365371
routeMetric := uint32(network.DefaultRouteMetric)
366372

367373
if metricStr := oneContext[ifaceName+"_METRIC"]; metricStr != "" {
@@ -462,10 +468,49 @@ func parseIPv6Gateway(oneContext map[string]string, ifaceName, ifaceNameLower st
462468
return nil
463469
}
464470

465-
// parseInterfaceIPv6 configures the static IPv6 stack for one interface.
466-
// Handles ETH*_IP6 (legacy: ETH*_IPV6), ETH*_IP6_PREFIX_LENGTH, ETH*_IP6_ULA,
467-
// ETH*_IP6_GATEWAY (legacy: ETH*_GATEWAY6), and ETH*_IP6_METRIC.
471+
// parseIPv6DHCP emits a DHCPv6 operator for an interface, with metric from
472+
// ETH*_IP6_METRIC (default 1).
473+
func parseIPv6DHCP(oneContext map[string]string, ifaceName, ifaceNameLower string, networkConfig *runtime.PlatformNetworkConfig) error {
474+
metric := uint32(1)
475+
476+
if metricStr := oneContext[ifaceName+"_IP6_METRIC"]; metricStr != "" {
477+
m, err := strconv.ParseUint(metricStr, 10, 32)
478+
if err != nil {
479+
return fmt.Errorf("interface %s: failed to parse IPv6 metric: %w", ifaceName, err)
480+
}
481+
482+
metric = uint32(m)
483+
}
484+
485+
networkConfig.Operators = append(networkConfig.Operators, network.OperatorSpecSpec{
486+
Operator: network.OperatorDHCP6,
487+
LinkName: ifaceNameLower,
488+
RequireUp: true,
489+
DHCP6: network.DHCP6OperatorSpec{
490+
RouteMetric: metric,
491+
SkipHostnameRequest: true,
492+
},
493+
ConfigLayer: network.ConfigPlatform,
494+
})
495+
496+
return nil
497+
}
498+
499+
// parseInterfaceIPv6 configures the IPv6 stack for one interface.
500+
// Dispatches on ETH*_IP6_METHOD: disable (skip), auto (SLAAC via kernel),
501+
// dhcp (DHCPv6 operator), or static/empty (Phase 2 static path).
468502
func parseInterfaceIPv6(oneContext map[string]string, ifaceName, ifaceNameLower string, networkConfig *runtime.PlatformNetworkConfig) error {
503+
switch strings.ToLower(oneContext[ifaceName+"_IP6_METHOD"]) {
504+
case "disable", methodSkip:
505+
return nil
506+
case "auto":
507+
// SLAAC: the kernel accepts Router Advertisements by default in Talos;
508+
// no operator or sysctl is required to enable address auto-configuration.
509+
return nil
510+
case "dhcp":
511+
return parseIPv6DHCP(oneContext, ifaceName, ifaceNameLower, networkConfig)
512+
}
513+
469514
ip6Str := oneContext[ifaceName+"_IP6"]
470515
if ip6Str == "" {
471516
ip6Str = oneContext[ifaceName+"_IPV6"]
@@ -512,6 +557,11 @@ func parseInterfaceIPv6(oneContext map[string]string, ifaceName, ifaceNameLower
512557
func parseInterface(oneContext map[string]string, ifaceName string, networkConfig *runtime.PlatformNetworkConfig, allDNSIPs *[]netip.Addr, allSearchDomains *[]string) error {
513558
ifaceNameLower := strings.ToLower(ifaceName)
514559

560+
ip6Method := strings.ToLower(oneContext[ifaceName+"_IP6_METHOD"])
561+
if oneContext[ifaceName+"_METHOD"] == methodSkip && (ip6Method == "" || ip6Method == "disable" || ip6Method == methodSkip) {
562+
return nil
563+
}
564+
515565
if err := parseInterfaceIPv4(oneContext, ifaceName, ifaceNameLower, networkConfig, allDNSIPs, allSearchDomains); err != nil {
516566
return err
517567
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package opennebula_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/cosi-project/runtime/pkg/state"
11+
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
12+
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula"
17+
"github.com/siderolabs/talos/pkg/machinery/resources/network"
18+
)
19+
20+
const skipContextBase = `ETH0_MAC = "02:00:c0:a8:01:5c"
21+
ETH0_IP = "192.168.1.92"
22+
ETH0_MASK = "255.255.255.0"
23+
NAME = "test"
24+
`
25+
26+
func skipContext(extra string) []byte {
27+
return []byte(skipContextBase + extra)
28+
}
29+
30+
func TestParseMethodSkip(t *testing.T) {
31+
t.Parallel()
32+
33+
o := &opennebula.OpenNebula{}
34+
st := state.WrapCore(namespaced.NewState(inmem.Build))
35+
36+
for _, tc := range []struct {
37+
name string
38+
extra string
39+
wantAddrs int
40+
wantLinks int
41+
wantRoutes int
42+
wantOperators []network.OperatorSpecSpec
43+
}{
44+
{
45+
name: "METHOD=skip omits interface entirely",
46+
extra: `ETH0_METHOD = "skip"`,
47+
wantAddrs: 0,
48+
wantLinks: 0,
49+
wantRoutes: 0,
50+
},
51+
{
52+
name: "METHOD=skip with IP6_METHOD=dhcp emits DHCPv6 operator only",
53+
extra: "ETH0_METHOD = \"skip\"\nETH0_IP6_METHOD = \"dhcp\"",
54+
wantOperators: []network.OperatorSpecSpec{
55+
{
56+
Operator: network.OperatorDHCP6,
57+
LinkName: "eth0",
58+
RequireUp: true,
59+
DHCP6: network.DHCP6OperatorSpec{
60+
RouteMetric: 1,
61+
SkipHostnameRequest: true,
62+
},
63+
ConfigLayer: network.ConfigPlatform,
64+
},
65+
},
66+
},
67+
{
68+
name: "METHOD=skip with IP6_METHOD=disable omits interface entirely",
69+
extra: "ETH0_METHOD = \"skip\"\nETH0_IP6_METHOD = \"disable\"",
70+
wantAddrs: 0,
71+
wantLinks: 0,
72+
wantRoutes: 0,
73+
},
74+
{
75+
name: "METHOD=skip with IP6_METHOD=skip omits interface entirely",
76+
extra: "ETH0_METHOD = \"skip\"\nETH0_IP6_METHOD = \"skip\"",
77+
wantAddrs: 0,
78+
wantLinks: 0,
79+
wantRoutes: 0,
80+
},
81+
} {
82+
t.Run(tc.name, func(t *testing.T) {
83+
t.Parallel()
84+
85+
networkConfig, err := o.ParseMetadata(st, skipContext(tc.extra))
86+
require.NoError(t, err)
87+
88+
assert.Len(t, networkConfig.Addresses, tc.wantAddrs)
89+
assert.Len(t, networkConfig.Links, tc.wantLinks)
90+
assert.Len(t, networkConfig.Routes, tc.wantRoutes)
91+
assert.Equal(t, tc.wantOperators, networkConfig.Operators)
92+
})
93+
}
94+
}

0 commit comments

Comments
 (0)