mirror of
https://github.com/siderolabs/talos.git
synced 2026-05-05 20:36:18 +02:00
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>
This commit is contained in:
parent
ad3c59aada
commit
23c99a3cb4
@ -36,9 +36,126 @@ func (o *OpenNebula) Name() string {
|
||||
return "opennebula"
|
||||
}
|
||||
|
||||
// parseRouteFields extracts the destination prefix, gateway string, and optional
|
||||
// metric string from the fields of a single route entry.
|
||||
//
|
||||
// The reference one-apps implementation (context-linux) always parses routes as:
|
||||
//
|
||||
// rsplit=( ${route} ); dst="${rsplit[0]}"; gw="${rsplit[2]}"
|
||||
//
|
||||
// meaning token[1] is always skipped and the gateway is always at token[2].
|
||||
// The canonical format is "DEST/PREFIX via GW" where "via" occupies token[1].
|
||||
// The legacy dotted-mask format "DEST MASK GW" follows the same index layout.
|
||||
//
|
||||
// As a Talos extension, an optional bare metric may follow the gateway.
|
||||
func parseRouteFields(parts []string) (dest netip.Prefix, gwStr, metricStr string, err error) {
|
||||
// Both CIDR ("DEST/PREFIX via GW") and legacy ("DEST MASK GW") formats
|
||||
// require at least 3 tokens, with the gateway always at index 2.
|
||||
if len(parts) < 3 {
|
||||
return dest, "", "", fmt.Errorf("expected at least 3 fields (DEST/PREFIX via GW or DEST MASK GW)")
|
||||
}
|
||||
|
||||
if strings.Contains(parts[0], "/") {
|
||||
// CIDR format: "DEST/PREFIX via GW [METRIC]"
|
||||
// parts[1] is the separator token (conventionally "via") and is skipped,
|
||||
// matching the reference rsplit[1] which is never read.
|
||||
dest, err = netip.ParsePrefix(parts[0])
|
||||
if err != nil {
|
||||
return dest, "", "", fmt.Errorf("failed to parse destination: %w", err)
|
||||
}
|
||||
|
||||
dest = dest.Masked()
|
||||
} else {
|
||||
// Legacy format: "DEST MASK GW [METRIC]"
|
||||
var prefix netip.Prefix
|
||||
|
||||
prefix, err = address.IPPrefixFrom(parts[0], parts[1])
|
||||
if err != nil {
|
||||
return dest, "", "", fmt.Errorf("failed to parse destination: %w", err)
|
||||
}
|
||||
|
||||
dest = prefix.Masked()
|
||||
}
|
||||
|
||||
gwStr = parts[2]
|
||||
|
||||
if len(parts) >= 4 {
|
||||
metricStr = parts[3]
|
||||
}
|
||||
|
||||
return dest, gwStr, metricStr, nil
|
||||
}
|
||||
|
||||
// parseRouteEntry parses a single trimmed route entry into a RouteSpecSpec.
|
||||
func parseRouteEntry(entry, linkName string) (network.RouteSpecSpec, error) {
|
||||
dest, gwStr, metricStr, err := parseRouteFields(strings.Fields(entry))
|
||||
if err != nil {
|
||||
return network.RouteSpecSpec{}, fmt.Errorf("route entry %q: %w", entry, err)
|
||||
}
|
||||
|
||||
gw, err := netip.ParseAddr(gwStr)
|
||||
if err != nil {
|
||||
return network.RouteSpecSpec{}, fmt.Errorf("route entry %q: failed to parse gateway: %w", entry, err)
|
||||
}
|
||||
|
||||
metric := uint32(network.DefaultRouteMetric)
|
||||
|
||||
if metricStr != "" {
|
||||
m, err := strconv.ParseUint(metricStr, 10, 32)
|
||||
if err != nil {
|
||||
return network.RouteSpecSpec{}, fmt.Errorf("route entry %q: failed to parse metric: %w", entry, err)
|
||||
}
|
||||
|
||||
metric = uint32(m)
|
||||
}
|
||||
|
||||
family := nethelpers.FamilyInet4
|
||||
if gw.Is6() {
|
||||
family = nethelpers.FamilyInet6
|
||||
}
|
||||
|
||||
route := network.RouteSpecSpec{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: dest,
|
||||
Gateway: gw,
|
||||
OutLinkName: linkName,
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: family,
|
||||
Priority: metric,
|
||||
}
|
||||
|
||||
route.Normalize()
|
||||
|
||||
return route, nil
|
||||
}
|
||||
|
||||
// ParseRoutes parses the ETH*_ROUTES variable into RouteSpecSpec entries.
|
||||
// Multiple routes are separated by commas.
|
||||
func ParseRoutes(routesStr, linkName string) ([]network.RouteSpecSpec, error) {
|
||||
var routes []network.RouteSpecSpec
|
||||
|
||||
for entry := range strings.SplitSeq(routesStr, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
route, err := parseRouteEntry(entry, linkName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes = append(routes, route)
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// ParseMetadata converts opennebula metadata to platform network config.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
//nolint:gocyclo,cyclop
|
||||
func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*runtime.PlatformNetworkConfig, error) {
|
||||
// Initialize the PlatformNetworkConfig
|
||||
networkConfig := &runtime.PlatformNetworkConfig{}
|
||||
@ -156,6 +273,15 @@ func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*run
|
||||
networkConfig.Routes = append(networkConfig.Routes, route)
|
||||
}
|
||||
|
||||
if routesStr := oneContext[ifaceName+"_ROUTES"]; routesStr != "" {
|
||||
staticRoutes, err := ParseRoutes(routesStr, ifaceNameLower)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interface %s: %w", ifaceName, err)
|
||||
}
|
||||
|
||||
networkConfig.Routes = append(networkConfig.Routes, staticRoutes...)
|
||||
}
|
||||
|
||||
// Parse DNS servers
|
||||
dnsServers := strings.Fields(oneContext[ifaceName+"_DNS"])
|
||||
|
||||
|
||||
@ -0,0 +1,219 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package opennebula_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula"
|
||||
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
|
||||
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
||||
)
|
||||
|
||||
func TestParseRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
routesStr string
|
||||
linkName string
|
||||
expected []network.RouteSpecSpec
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
routesStr: "",
|
||||
linkName: "eth0",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
routesStr: " , , ",
|
||||
linkName: "eth0",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "legacy single route default metric",
|
||||
routesStr: "10.0.0.0 255.0.0.0 192.168.1.1",
|
||||
linkName: "eth0",
|
||||
expected: []network.RouteSpecSpec{
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: network.DefaultRouteMetric,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy single route custom metric",
|
||||
routesStr: "172.16.0.0 255.255.0.0 192.168.1.1 500",
|
||||
linkName: "eth0",
|
||||
expected: []network.RouteSpecSpec{
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("172.16.0.0/16"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: 500,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cidr single route",
|
||||
routesStr: "10.0.0.0/8 via 192.168.1.1",
|
||||
linkName: "eth0",
|
||||
expected: []network.RouteSpecSpec{
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: network.DefaultRouteMetric,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cidr single route with metric",
|
||||
routesStr: "10.0.0.0/8 via 192.168.1.1 200",
|
||||
linkName: "eth0",
|
||||
expected: []network.RouteSpecSpec{
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: 200,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple routes comma separated",
|
||||
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",
|
||||
linkName: "eth0",
|
||||
expected: []network.RouteSpecSpec{
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: network.DefaultRouteMetric,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("172.16.0.0/16"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: 500,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cidr host bits masked",
|
||||
routesStr: "10.1.2.0/8 via 192.168.1.1",
|
||||
linkName: "eth0",
|
||||
expected: []network.RouteSpecSpec{
|
||||
{
|
||||
ConfigLayer: network.ConfigPlatform,
|
||||
Destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||
Gateway: netip.MustParseAddr("192.168.1.1"),
|
||||
OutLinkName: "eth0",
|
||||
Table: nethelpers.TableMain,
|
||||
Protocol: nethelpers.ProtocolStatic,
|
||||
Type: nethelpers.TypeUnicast,
|
||||
Family: nethelpers.FamilyInet4,
|
||||
Priority: network.DefaultRouteMetric,
|
||||
Scope: nethelpers.ScopeGlobal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "malformed gateway",
|
||||
routesStr: "10.0.0.0/8 via notanip",
|
||||
linkName: "eth0",
|
||||
errMsg: "failed to parse gateway",
|
||||
},
|
||||
{
|
||||
name: "malformed cidr destination",
|
||||
routesStr: "notaprefix/8 via 192.168.1.1",
|
||||
linkName: "eth0",
|
||||
errMsg: "failed to parse destination",
|
||||
},
|
||||
{
|
||||
name: "malformed legacy destination",
|
||||
routesStr: "notanip 255.0.0.0 192.168.1.1",
|
||||
linkName: "eth0",
|
||||
errMsg: "failed to parse destination",
|
||||
},
|
||||
{
|
||||
name: "malformed metric",
|
||||
routesStr: "10.0.0.0/8 via 192.168.1.1 notanumber",
|
||||
linkName: "eth0",
|
||||
errMsg: "failed to parse metric",
|
||||
},
|
||||
{
|
||||
name: "too few fields",
|
||||
routesStr: "10.0.0.0/8 via",
|
||||
linkName: "eth0",
|
||||
errMsg: "expected at least 3 fields",
|
||||
},
|
||||
{
|
||||
name: "legacy too few fields",
|
||||
routesStr: "10.0.0.0 255.0.0.0",
|
||||
linkName: "eth0",
|
||||
errMsg: "expected at least 3 fields",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
routes, err := opennebula.ParseRoutes(tc.routesStr, tc.linkName)
|
||||
|
||||
if tc.errMsg != "" {
|
||||
require.ErrorContains(t, err, tc.errMsg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, routes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,30 @@ routes:
|
||||
flags: ""
|
||||
protocol: static
|
||||
layer: platform
|
||||
- family: inet4
|
||||
dst: 10.0.0.0/8
|
||||
src: ""
|
||||
gateway: 192.168.1.1
|
||||
outLinkName: eth0
|
||||
table: main
|
||||
priority: 1024
|
||||
scope: global
|
||||
type: unicast
|
||||
flags: ""
|
||||
protocol: static
|
||||
layer: platform
|
||||
- family: inet4
|
||||
dst: 172.16.0.0/16
|
||||
src: ""
|
||||
gateway: 192.168.1.1
|
||||
outLinkName: eth0
|
||||
table: main
|
||||
priority: 500
|
||||
scope: global
|
||||
type: unicast
|
||||
flags: ""
|
||||
protocol: static
|
||||
layer: platform
|
||||
hostnames:
|
||||
- hostname: code-server
|
||||
domainname: ""
|
||||
|
||||
@ -26,6 +26,30 @@ routes:
|
||||
flags: ""
|
||||
protocol: static
|
||||
layer: platform
|
||||
- family: inet4
|
||||
dst: 10.0.0.0/8
|
||||
src: ""
|
||||
gateway: 192.168.1.1
|
||||
outLinkName: eth0
|
||||
table: main
|
||||
priority: 1024
|
||||
scope: global
|
||||
type: unicast
|
||||
flags: ""
|
||||
protocol: static
|
||||
layer: platform
|
||||
- family: inet4
|
||||
dst: 172.16.0.0/16
|
||||
src: ""
|
||||
gateway: 192.168.1.1
|
||||
outLinkName: eth0
|
||||
table: main
|
||||
priority: 500
|
||||
scope: global
|
||||
type: unicast
|
||||
flags: ""
|
||||
protocol: static
|
||||
layer: platform
|
||||
hostnames:
|
||||
- hostname: code-server
|
||||
domainname: ""
|
||||
|
||||
@ -16,6 +16,7 @@ ETH0_METHOD = ""
|
||||
ETH0_METRIC = ""
|
||||
ETH0_MTU = ""
|
||||
ETH0_NETWORK = "192.168.1.0"
|
||||
ETH0_ROUTES = "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"
|
||||
ETH0_SEARCH_DOMAIN = ""
|
||||
ETH0_VLAN_ID = "3"
|
||||
ETH0_VROUTER_IP = ""
|
||||
|
||||
@ -18,6 +18,7 @@ ETH0_METHOD = ""
|
||||
ETH0_METRIC = ""
|
||||
ETH0_MTU = ""
|
||||
ETH0_NETWORK = "192.168.1.0"
|
||||
ETH0_ROUTES = "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"
|
||||
ETH0_SEARCH_DOMAIN = ""
|
||||
ETH0_VLAN_ID = "3"
|
||||
ETH0_VROUTER_IP = ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user