Skip to content

Commit 04fba03

Browse files
mcanevetsmira
authored andcommitted
feat(machined): add static routes support via ETH*_ROUTES for OpenNebula
Parse the ETH*_ROUTES context variable in the OpenNebula platform and install per-interface static routes into the platform network config. Both legacy format ("DEST MASK GW [METRIC]") and CIDR format ("DEST/PREFIX GW [METRIC]") are supported, matching the reference one-apps context-linux implementation. Signed-off-by: Mickaël Canévet <mickael.canevet@proton.ch> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com> (cherry picked from commit 23c99a3)
1 parent da6c6e4 commit 04fba03

File tree

6 files changed

+396
-1
lines changed

6 files changed

+396
-1
lines changed

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

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,126 @@ func (o *OpenNebula) Name() string {
3636
return "opennebula"
3737
}
3838

39+
// parseRouteFields extracts the destination prefix, gateway string, and optional
40+
// metric string from the fields of a single route entry.
41+
//
42+
// The reference one-apps implementation (context-linux) always parses routes as:
43+
//
44+
// rsplit=( ${route} ); dst="${rsplit[0]}"; gw="${rsplit[2]}"
45+
//
46+
// meaning token[1] is always skipped and the gateway is always at token[2].
47+
// The canonical format is "DEST/PREFIX via GW" where "via" occupies token[1].
48+
// The legacy dotted-mask format "DEST MASK GW" follows the same index layout.
49+
//
50+
// As a Talos extension, an optional bare metric may follow the gateway.
51+
func parseRouteFields(parts []string) (dest netip.Prefix, gwStr, metricStr string, err error) {
52+
// Both CIDR ("DEST/PREFIX via GW") and legacy ("DEST MASK GW") formats
53+
// require at least 3 tokens, with the gateway always at index 2.
54+
if len(parts) < 3 {
55+
return dest, "", "", fmt.Errorf("expected at least 3 fields (DEST/PREFIX via GW or DEST MASK GW)")
56+
}
57+
58+
if strings.Contains(parts[0], "/") {
59+
// CIDR format: "DEST/PREFIX via GW [METRIC]"
60+
// parts[1] is the separator token (conventionally "via") and is skipped,
61+
// matching the reference rsplit[1] which is never read.
62+
dest, err = netip.ParsePrefix(parts[0])
63+
if err != nil {
64+
return dest, "", "", fmt.Errorf("failed to parse destination: %w", err)
65+
}
66+
67+
dest = dest.Masked()
68+
} else {
69+
// Legacy format: "DEST MASK GW [METRIC]"
70+
var prefix netip.Prefix
71+
72+
prefix, err = address.IPPrefixFrom(parts[0], parts[1])
73+
if err != nil {
74+
return dest, "", "", fmt.Errorf("failed to parse destination: %w", err)
75+
}
76+
77+
dest = prefix.Masked()
78+
}
79+
80+
gwStr = parts[2]
81+
82+
if len(parts) >= 4 {
83+
metricStr = parts[3]
84+
}
85+
86+
return dest, gwStr, metricStr, nil
87+
}
88+
89+
// parseRouteEntry parses a single trimmed route entry into a RouteSpecSpec.
90+
func parseRouteEntry(entry, linkName string) (network.RouteSpecSpec, error) {
91+
dest, gwStr, metricStr, err := parseRouteFields(strings.Fields(entry))
92+
if err != nil {
93+
return network.RouteSpecSpec{}, fmt.Errorf("route entry %q: %w", entry, err)
94+
}
95+
96+
gw, err := netip.ParseAddr(gwStr)
97+
if err != nil {
98+
return network.RouteSpecSpec{}, fmt.Errorf("route entry %q: failed to parse gateway: %w", entry, err)
99+
}
100+
101+
metric := uint32(network.DefaultRouteMetric)
102+
103+
if metricStr != "" {
104+
m, err := strconv.ParseUint(metricStr, 10, 32)
105+
if err != nil {
106+
return network.RouteSpecSpec{}, fmt.Errorf("route entry %q: failed to parse metric: %w", entry, err)
107+
}
108+
109+
metric = uint32(m)
110+
}
111+
112+
family := nethelpers.FamilyInet4
113+
if gw.Is6() {
114+
family = nethelpers.FamilyInet6
115+
}
116+
117+
route := network.RouteSpecSpec{
118+
ConfigLayer: network.ConfigPlatform,
119+
Destination: dest,
120+
Gateway: gw,
121+
OutLinkName: linkName,
122+
Table: nethelpers.TableMain,
123+
Protocol: nethelpers.ProtocolStatic,
124+
Type: nethelpers.TypeUnicast,
125+
Family: family,
126+
Priority: metric,
127+
}
128+
129+
route.Normalize()
130+
131+
return route, nil
132+
}
133+
134+
// ParseRoutes parses the ETH*_ROUTES variable into RouteSpecSpec entries.
135+
// Multiple routes are separated by commas.
136+
func ParseRoutes(routesStr, linkName string) ([]network.RouteSpecSpec, error) {
137+
var routes []network.RouteSpecSpec
138+
139+
for entry := range strings.SplitSeq(routesStr, ",") {
140+
entry = strings.TrimSpace(entry)
141+
if entry == "" {
142+
continue
143+
}
144+
145+
route, err := parseRouteEntry(entry, linkName)
146+
if err != nil {
147+
return nil, err
148+
}
149+
150+
routes = append(routes, route)
151+
}
152+
153+
return routes, nil
154+
}
155+
39156
// ParseMetadata converts opennebula metadata to platform network config.
40157
//
41-
//nolint:gocyclo
158+
//nolint:gocyclo,cyclop
42159
func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*runtime.PlatformNetworkConfig, error) {
43160
// Initialize the PlatformNetworkConfig
44161
networkConfig := &runtime.PlatformNetworkConfig{}
@@ -156,6 +273,15 @@ func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*run
156273
networkConfig.Routes = append(networkConfig.Routes, route)
157274
}
158275

276+
if routesStr := oneContext[ifaceName+"_ROUTES"]; routesStr != "" {
277+
staticRoutes, err := ParseRoutes(routesStr, ifaceNameLower)
278+
if err != nil {
279+
return nil, fmt.Errorf("interface %s: %w", ifaceName, err)
280+
}
281+
282+
networkConfig.Routes = append(networkConfig.Routes, staticRoutes...)
283+
}
284+
159285
// Parse DNS servers
160286
dnsServers := strings.Fields(oneContext[ifaceName+"_DNS"])
161287

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
"net/netip"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula"
15+
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
16+
"github.com/siderolabs/talos/pkg/machinery/resources/network"
17+
)
18+
19+
func TestParseRoutes(t *testing.T) {
20+
t.Parallel()
21+
22+
for _, tc := range []struct {
23+
name string
24+
routesStr string
25+
linkName string
26+
expected []network.RouteSpecSpec
27+
errMsg string
28+
}{
29+
{
30+
name: "empty string",
31+
routesStr: "",
32+
linkName: "eth0",
33+
expected: nil,
34+
},
35+
{
36+
name: "whitespace only",
37+
routesStr: " , , ",
38+
linkName: "eth0",
39+
expected: nil,
40+
},
41+
{
42+
name: "legacy single route default metric",
43+
routesStr: "10.0.0.0 255.0.0.0 192.168.1.1",
44+
linkName: "eth0",
45+
expected: []network.RouteSpecSpec{
46+
{
47+
ConfigLayer: network.ConfigPlatform,
48+
Destination: netip.MustParsePrefix("10.0.0.0/8"),
49+
Gateway: netip.MustParseAddr("192.168.1.1"),
50+
OutLinkName: "eth0",
51+
Table: nethelpers.TableMain,
52+
Protocol: nethelpers.ProtocolStatic,
53+
Type: nethelpers.TypeUnicast,
54+
Family: nethelpers.FamilyInet4,
55+
Priority: network.DefaultRouteMetric,
56+
Scope: nethelpers.ScopeGlobal,
57+
},
58+
},
59+
},
60+
{
61+
name: "legacy single route custom metric",
62+
routesStr: "172.16.0.0 255.255.0.0 192.168.1.1 500",
63+
linkName: "eth0",
64+
expected: []network.RouteSpecSpec{
65+
{
66+
ConfigLayer: network.ConfigPlatform,
67+
Destination: netip.MustParsePrefix("172.16.0.0/16"),
68+
Gateway: netip.MustParseAddr("192.168.1.1"),
69+
OutLinkName: "eth0",
70+
Table: nethelpers.TableMain,
71+
Protocol: nethelpers.ProtocolStatic,
72+
Type: nethelpers.TypeUnicast,
73+
Family: nethelpers.FamilyInet4,
74+
Priority: 500,
75+
Scope: nethelpers.ScopeGlobal,
76+
},
77+
},
78+
},
79+
{
80+
name: "cidr single route",
81+
routesStr: "10.0.0.0/8 via 192.168.1.1",
82+
linkName: "eth0",
83+
expected: []network.RouteSpecSpec{
84+
{
85+
ConfigLayer: network.ConfigPlatform,
86+
Destination: netip.MustParsePrefix("10.0.0.0/8"),
87+
Gateway: netip.MustParseAddr("192.168.1.1"),
88+
OutLinkName: "eth0",
89+
Table: nethelpers.TableMain,
90+
Protocol: nethelpers.ProtocolStatic,
91+
Type: nethelpers.TypeUnicast,
92+
Family: nethelpers.FamilyInet4,
93+
Priority: network.DefaultRouteMetric,
94+
Scope: nethelpers.ScopeGlobal,
95+
},
96+
},
97+
},
98+
{
99+
name: "cidr single route with metric",
100+
routesStr: "10.0.0.0/8 via 192.168.1.1 200",
101+
linkName: "eth0",
102+
expected: []network.RouteSpecSpec{
103+
{
104+
ConfigLayer: network.ConfigPlatform,
105+
Destination: netip.MustParsePrefix("10.0.0.0/8"),
106+
Gateway: netip.MustParseAddr("192.168.1.1"),
107+
OutLinkName: "eth0",
108+
Table: nethelpers.TableMain,
109+
Protocol: nethelpers.ProtocolStatic,
110+
Type: nethelpers.TypeUnicast,
111+
Family: nethelpers.FamilyInet4,
112+
Priority: 200,
113+
Scope: nethelpers.ScopeGlobal,
114+
},
115+
},
116+
},
117+
{
118+
name: "multiple routes comma separated",
119+
routesStr: "10.0.0.0 255.0.0.0 192.168.1.1, 172.16.0.0 255.255.0.0 192.168.1.1 500",
120+
linkName: "eth0",
121+
expected: []network.RouteSpecSpec{
122+
{
123+
ConfigLayer: network.ConfigPlatform,
124+
Destination: netip.MustParsePrefix("10.0.0.0/8"),
125+
Gateway: netip.MustParseAddr("192.168.1.1"),
126+
OutLinkName: "eth0",
127+
Table: nethelpers.TableMain,
128+
Protocol: nethelpers.ProtocolStatic,
129+
Type: nethelpers.TypeUnicast,
130+
Family: nethelpers.FamilyInet4,
131+
Priority: network.DefaultRouteMetric,
132+
Scope: nethelpers.ScopeGlobal,
133+
},
134+
{
135+
ConfigLayer: network.ConfigPlatform,
136+
Destination: netip.MustParsePrefix("172.16.0.0/16"),
137+
Gateway: netip.MustParseAddr("192.168.1.1"),
138+
OutLinkName: "eth0",
139+
Table: nethelpers.TableMain,
140+
Protocol: nethelpers.ProtocolStatic,
141+
Type: nethelpers.TypeUnicast,
142+
Family: nethelpers.FamilyInet4,
143+
Priority: 500,
144+
Scope: nethelpers.ScopeGlobal,
145+
},
146+
},
147+
},
148+
{
149+
name: "cidr host bits masked",
150+
routesStr: "10.1.2.0/8 via 192.168.1.1",
151+
linkName: "eth0",
152+
expected: []network.RouteSpecSpec{
153+
{
154+
ConfigLayer: network.ConfigPlatform,
155+
Destination: netip.MustParsePrefix("10.0.0.0/8"),
156+
Gateway: netip.MustParseAddr("192.168.1.1"),
157+
OutLinkName: "eth0",
158+
Table: nethelpers.TableMain,
159+
Protocol: nethelpers.ProtocolStatic,
160+
Type: nethelpers.TypeUnicast,
161+
Family: nethelpers.FamilyInet4,
162+
Priority: network.DefaultRouteMetric,
163+
Scope: nethelpers.ScopeGlobal,
164+
},
165+
},
166+
},
167+
{
168+
name: "malformed gateway",
169+
routesStr: "10.0.0.0/8 via notanip",
170+
linkName: "eth0",
171+
errMsg: "failed to parse gateway",
172+
},
173+
{
174+
name: "malformed cidr destination",
175+
routesStr: "notaprefix/8 via 192.168.1.1",
176+
linkName: "eth0",
177+
errMsg: "failed to parse destination",
178+
},
179+
{
180+
name: "malformed legacy destination",
181+
routesStr: "notanip 255.0.0.0 192.168.1.1",
182+
linkName: "eth0",
183+
errMsg: "failed to parse destination",
184+
},
185+
{
186+
name: "malformed metric",
187+
routesStr: "10.0.0.0/8 via 192.168.1.1 notanumber",
188+
linkName: "eth0",
189+
errMsg: "failed to parse metric",
190+
},
191+
{
192+
name: "too few fields",
193+
routesStr: "10.0.0.0/8 via",
194+
linkName: "eth0",
195+
errMsg: "expected at least 3 fields",
196+
},
197+
{
198+
name: "legacy too few fields",
199+
routesStr: "10.0.0.0 255.0.0.0",
200+
linkName: "eth0",
201+
errMsg: "expected at least 3 fields",
202+
},
203+
} {
204+
t.Run(tc.name, func(t *testing.T) {
205+
t.Parallel()
206+
207+
routes, err := opennebula.ParseRoutes(tc.routesStr, tc.linkName)
208+
209+
if tc.errMsg != "" {
210+
require.ErrorContains(t, err, tc.errMsg)
211+
212+
return
213+
}
214+
215+
require.NoError(t, err)
216+
assert.Equal(t, tc.expected, routes)
217+
})
218+
}
219+
}

internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula/testdata/expected.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,30 @@ routes:
2626
flags: ""
2727
protocol: static
2828
layer: platform
29+
- family: inet4
30+
dst: 10.0.0.0/8
31+
src: ""
32+
gateway: 192.168.1.1
33+
outLinkName: eth0
34+
table: main
35+
priority: 1024
36+
scope: global
37+
type: unicast
38+
flags: ""
39+
protocol: static
40+
layer: platform
41+
- family: inet4
42+
dst: 172.16.0.0/16
43+
src: ""
44+
gateway: 192.168.1.1
45+
outLinkName: eth0
46+
table: main
47+
priority: 500
48+
scope: global
49+
type: unicast
50+
flags: ""
51+
protocol: static
52+
layer: platform
2953
hostnames:
3054
- hostname: code-server
3155
domainname: ""

0 commit comments

Comments
 (0)