chore: use shared ExternalPoolConfig type + fix ip math

This commit is contained in:
Julian Sommer
2026-07-01 14:05:19 +10:00
parent 7bbd99c792
commit bae2dfe30f
10 changed files with 90 additions and 171 deletions
+3 -2
View File
@@ -23,6 +23,7 @@ import (
"strings"
"github.com/mulgadc/spinifex/spinifex/config"
"github.com/mulgadc/spinifex/spinifex/network/external"
"github.com/mulgadc/spinifex/spinifex/service"
"github.com/mulgadc/spinifex/spinifex/services/awsgw"
"github.com/mulgadc/spinifex/spinifex/services/nats"
@@ -730,9 +731,9 @@ var vpcdStartCmd = &cobra.Command{
nodeConfig := clusterConfig.Nodes[clusterConfig.Node]
// Map cluster-wide external pools to vpcd config
var extPools []vpcd.ExternalPoolConfig
var extPools []external.ExternalPoolConfig
for _, p := range clusterConfig.Network.ExternalPools {
extPools = append(extPools, vpcd.ExternalPoolConfig{
extPools = append(extPools, external.ExternalPoolConfig{
Name: p.Name,
Source: p.Source,
BindBridge: p.BindBridge,
+3 -2
View File
@@ -55,6 +55,7 @@ import (
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/instancetypes"
"github.com/mulgadc/spinifex/spinifex/network/external"
"github.com/mulgadc/spinifex/spinifex/network/external/dhcp"
"github.com/mulgadc/spinifex/spinifex/network/host"
"github.com/mulgadc/spinifex/spinifex/objectstore"
@@ -1307,10 +1308,10 @@ func (d *Daemon) startCluster() error {
if jsErr != nil {
slog.Warn("Failed to get JetStream for external IPAM", "err", jsErr)
} else {
var pools []handlers_ec2_vpc.ExternalPoolConfig
var pools []external.ExternalPoolConfig
anyDHCP := false
for _, p := range d.clusterConfig.Network.ExternalPools {
pools = append(pools, handlers_ec2_vpc.ExternalPoolConfig{
pools = append(pools, external.ExternalPoolConfig{
Name: p.Name,
Source: p.Source,
BindBridge: p.BindBridge,
+2 -1
View File
@@ -29,6 +29,7 @@ import (
handlers_ec2_tags "github.com/mulgadc/spinifex/spinifex/handlers/ec2/tags"
handlers_ec2_volume "github.com/mulgadc/spinifex/spinifex/handlers/ec2/volume"
handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc"
"github.com/mulgadc/spinifex/spinifex/network/external"
"github.com/mulgadc/spinifex/spinifex/objectstore"
"github.com/mulgadc/spinifex/spinifex/qmp"
"github.com/mulgadc/spinifex/spinifex/testutil"
@@ -3535,7 +3536,7 @@ func TestDelegateHandlers_EIP(t *testing.T) {
js, err := nc.JetStream()
require.NoError(t, err)
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{
{Name: "test-pool", RangeStart: "192.168.100.2", RangeEnd: "192.168.100.254", Gateway: "192.168.100.1", PrefixLen: 24},
})
require.NoError(t, err)
@@ -14,6 +14,7 @@ import (
handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip"
handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc"
"github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
"github.com/mulgadc/spinifex/spinifex/network/external"
"github.com/mulgadc/spinifex/spinifex/tags"
"github.com/mulgadc/spinifex/spinifex/testutil"
"github.com/mulgadc/spinifex/spinifex/vm"
@@ -496,7 +497,7 @@ func TestLaunchSystemInstance_NATFailureRollsBackPublicIP(t *testing.T) {
js, err := jsNC.JetStream()
require.NoError(t, err)
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{
{Name: "wan-test", RangeStart: "203.0.113.10", RangeEnd: "203.0.113.20", Gateway: "203.0.113.1", PrefixLen: 24},
})
require.NoError(t, err)
@@ -618,7 +619,7 @@ func TestReleaseSystemInstanceEIP_ReleasesEipServiceAllocation(t *testing.T) {
js, err := jsNC.JetStream()
require.NoError(t, err)
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{
{Name: "wan-test", RangeStart: "203.0.113.10", RangeEnd: "203.0.113.20", Gateway: "203.0.113.1", PrefixLen: 24},
})
require.NoError(t, err)
@@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc"
"github.com/mulgadc/spinifex/spinifex/network/external"
"github.com/mulgadc/spinifex/spinifex/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -15,8 +16,8 @@ import (
const testAccountID = "123456789012"
func testPool() handlers_ec2_vpc.ExternalPoolConfig {
return handlers_ec2_vpc.ExternalPoolConfig{
func testPool() external.ExternalPoolConfig {
return external.ExternalPoolConfig{
Name: "test-pool",
RangeStart: "198.51.100.10",
RangeEnd: "198.51.100.20",
@@ -30,7 +31,7 @@ func setupTestEIP(t *testing.T) (*EIPServiceImpl, *handlers_ec2_vpc.ExternalIPAM
_, nc, js := testutil.StartTestJetStream(t)
pool := testPool()
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{pool})
ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{pool})
require.NoError(t, err)
svc, err := NewEIPServiceImpl(nc, ipam, nil)
+12 -58
View File
@@ -1,6 +1,7 @@
package handlers_ec2_vpc
import (
"cmp"
"context"
"errors"
"fmt"
@@ -25,31 +26,11 @@ const (
KVBucketExternalIPAMVersion = external.KVBucketStaticPoolVersion
)
// ExternalPoolConfig is the admin-defined IP pool from spinifex.toml.
// Duplicated from network/external.ExternalPoolConfig pending consolidation.
type ExternalPoolConfig struct {
Name string
Source string // "static" (default) or "dhcp"
BindBridge string // Linux bridge for DHCP DORA (source=dhcp only)
RangeStart string
RangeEnd string
Gateway string
GatewayIP string
PrefixLen int
Region string
AZ string
// GwLrpRangeStart/End reserves a sub-range of the LAN for OVN gateway
// LRP IPs in centralized NAT mode. IPAM must skip these addresses or
// the per-VM EIP allocator and vpcd will fight over them.
GwLrpRangeStart string
GwLrpRangeEnd string
}
// ExternalIPAM is the AWS-facing entry point for external IP allocation,
// dispatching to StaticPoolAllocator or dhcp.DHCPPoolAllocator per pool name.
type ExternalIPAM struct {
kv nats.KeyValue
pools []ExternalPoolConfig
pools []external.ExternalPoolConfig
static *external.StaticPoolAllocator
perPool map[string]external.Allocator // dhcp overrides; static pools fall through to `static`
}
@@ -57,7 +38,7 @@ type ExternalIPAM struct {
// NewExternalIPAM creates a new ExternalIPAM. Static pools wire through
// external.StaticPoolAllocator; DHCP-sourced pools wait for EnableDHCP
// to install the per-pool dhcp.DHCPPoolAllocator.
func NewExternalIPAM(js nats.JetStreamContext, pools []ExternalPoolConfig) (*ExternalIPAM, error) {
func NewExternalIPAM(js nats.JetStreamContext, pools []external.ExternalPoolConfig) (*ExternalIPAM, error) {
staticPools := filterStatic(pools)
var (
alloc *external.StaticPoolAllocator
@@ -65,7 +46,7 @@ func NewExternalIPAM(js nats.JetStreamContext, pools []ExternalPoolConfig) (*Ext
)
if len(staticPools) > 0 {
var err error
alloc, err = external.NewStaticPoolAllocator(js, toExternalPools(staticPools))
alloc, err = external.NewStaticPoolAllocator(js, staticPools)
if err != nil {
return nil, err
}
@@ -75,8 +56,8 @@ func NewExternalIPAM(js nats.JetStreamContext, pools []ExternalPoolConfig) (*Ext
}
// NewExternalIPAMWithKV creates an ExternalIPAM with an existing KV bucket (for testing).
func NewExternalIPAMWithKV(kv nats.KeyValue, pools []ExternalPoolConfig) *ExternalIPAM {
alloc := external.NewStaticPoolAllocatorWithKV(kv, toExternalPools(filterStatic(pools)))
func NewExternalIPAMWithKV(kv nats.KeyValue, pools []external.ExternalPoolConfig) *ExternalIPAM {
alloc := external.NewStaticPoolAllocatorWithKV(kv, filterStatic(pools))
return &ExternalIPAM{kv: kv, pools: pools, static: alloc, perPool: map[string]external.Allocator{}}
}
@@ -91,7 +72,7 @@ func (m *ExternalIPAM) EnableDHCP(client *dhcp.NATSClient) error {
if p.Source != external.SourceDHCP {
continue
}
m.perPool[p.Name] = dhcp.NewDHCPPoolAllocator(client, toExternalPools([]ExternalPoolConfig{p})[0])
m.perPool[p.Name] = dhcp.NewDHCPPoolAllocator(client, p)
}
return nil
}
@@ -165,7 +146,7 @@ func (m *ExternalIPAM) allocatorFor(poolName string) (external.Allocator, error)
// findPool returns the best pool for the given region/AZ using the same
// fallback order as topology.go: AZ-scoped → region-scoped → unscoped.
func (m *ExternalIPAM) findPool(region, az string) *ExternalPoolConfig {
func (m *ExternalIPAM) findPool(region, az string) *external.ExternalPoolConfig {
for i := range m.pools {
p := &m.pools[i]
if p.AZ != "" && p.AZ == az && p.Region == region {
@@ -187,34 +168,9 @@ func (m *ExternalIPAM) findPool(region, az string) *ExternalPoolConfig {
return nil
}
// toExternalPools converts the handlers-side ExternalPoolConfig into the
// network/external mirror. The duplicate type goes away in the deferred
// L5 cleanup.
func toExternalPools(pools []ExternalPoolConfig) []external.ExternalPoolConfig {
out := make([]external.ExternalPoolConfig, len(pools))
for i, p := range pools {
out[i] = external.ExternalPoolConfig{
Name: p.Name,
Source: p.Source,
BindBridge: p.BindBridge,
RangeStart: p.RangeStart,
RangeEnd: p.RangeEnd,
Gateway: p.Gateway,
GatewayIP: p.GatewayIP,
PrefixLen: p.PrefixLen,
DNSServers: nil,
Region: p.Region,
AZ: p.AZ,
GwLrpRangeStart: p.GwLrpRangeStart,
GwLrpRangeEnd: p.GwLrpRangeEnd,
}
}
return out
}
// filterStatic returns only the static pools (Source unset or "static").
func filterStatic(pools []ExternalPoolConfig) []ExternalPoolConfig {
out := make([]ExternalPoolConfig, 0, len(pools))
func filterStatic(pools []external.ExternalPoolConfig) []external.ExternalPoolConfig {
out := make([]external.ExternalPoolConfig, 0, len(pools))
for _, p := range pools {
if p.Source == "" || p.Source == external.SourceStatic {
out = append(out, p)
@@ -224,7 +180,7 @@ func filterStatic(pools []ExternalPoolConfig) []ExternalPoolConfig {
}
// ValidatePoolConfig checks that a pool config is valid.
func ValidatePoolConfig(pool ExternalPoolConfig) error {
func ValidatePoolConfig(pool external.ExternalPoolConfig) error {
if pool.Name == "" {
return fmt.Errorf("pool name is required")
}
@@ -271,7 +227,5 @@ func ValidatePoolConfig(pool ExternalPoolConfig) error {
}
func compareIPs(a, b net.IP) int {
ai := ipToInt(a.To4())
bi := ipToInt(b.To4())
return ai.Cmp(bi)
return cmp.Compare(ipToInt(a), ipToInt(b))
}
+46 -45
View File
@@ -4,12 +4,13 @@ import (
"sync"
"testing"
"github.com/mulgadc/spinifex/spinifex/network/external"
"github.com/mulgadc/spinifex/spinifex/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestExternalIPAM(t *testing.T, pools []ExternalPoolConfig) *ExternalIPAM {
func setupTestExternalIPAM(t *testing.T, pools []external.ExternalPoolConfig) *ExternalIPAM {
t.Helper()
_, _, js := testutil.StartTestJetStream(t)
@@ -18,8 +19,8 @@ func setupTestExternalIPAM(t *testing.T, pools []ExternalPoolConfig) *ExternalIP
return ipam
}
func testPool() ExternalPoolConfig {
return ExternalPoolConfig{
func testPool() external.ExternalPoolConfig {
return external.ExternalPoolConfig{
Name: "wan",
RangeStart: "192.168.1.150",
RangeEnd: "192.168.1.160",
@@ -30,7 +31,7 @@ func testPool() ExternalPoolConfig {
func TestExternalIPAM_AllocateSequential(t *testing.T) {
pool := testPool()
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
// .150 is reserved for gateway, so first allocable is .151
ip1, poolName, err := ipam.AllocateIP("", "", PurposeENIPublic, "", "eni-1", "i-1")
@@ -49,7 +50,7 @@ func TestExternalIPAM_AllocateSequential(t *testing.T) {
func TestExternalIPAM_GatewayIPReserved(t *testing.T) {
pool := testPool()
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
record, err := ipam.GetPoolRecord("wan")
require.NoError(t, err)
@@ -67,7 +68,7 @@ func TestExternalIPAM_GatewayIPReserved(t *testing.T) {
func TestExternalIPAM_ExplicitGatewayIP(t *testing.T) {
pool := testPool()
pool.GatewayIP = "192.168.1.155"
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
record, err := ipam.GetPoolRecord("wan")
require.NoError(t, err)
@@ -86,7 +87,7 @@ func TestExternalIPAM_ExplicitGatewayIP(t *testing.T) {
func TestExternalIPAM_Release(t *testing.T) {
pool := testPool()
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
ip1, _, err := ipam.AllocateIP("", "", PurposeENIPublic, "", "eni-1", "i-1")
require.NoError(t, err)
@@ -108,14 +109,14 @@ func TestExternalIPAM_Release(t *testing.T) {
}
func TestExternalIPAM_Exhaustion(t *testing.T) {
pool := ExternalPoolConfig{
pool := external.ExternalPoolConfig{
Name: "tiny",
RangeStart: "10.0.0.1",
RangeEnd: "10.0.0.3",
Gateway: "10.0.0.254",
PrefixLen: 24,
}
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
// .1 reserved for gateway, .2 and .3 allocable
ip1, _, err := ipam.AllocateIP("", "", PurposeENIPublic, "", "eni-1", "i-1")
@@ -133,7 +134,7 @@ func TestExternalIPAM_Exhaustion(t *testing.T) {
func TestExternalIPAM_CASConflict(t *testing.T) {
pool := testPool()
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
// Concurrent allocations should all succeed (CAS retry handles conflicts)
var wg sync.WaitGroup
@@ -165,7 +166,7 @@ func TestExternalIPAM_CASConflict(t *testing.T) {
}
func TestExternalIPAM_MultiPool(t *testing.T) {
pools := []ExternalPoolConfig{
pools := []external.ExternalPoolConfig{
{
Name: "us-east",
RangeStart: "203.0.113.2",
@@ -201,7 +202,7 @@ func TestExternalIPAM_MultiPool(t *testing.T) {
}
func TestExternalIPAM_PoolFallback(t *testing.T) {
pools := []ExternalPoolConfig{
pools := []external.ExternalPoolConfig{
{
Name: "az-pool",
RangeStart: "10.0.0.1",
@@ -249,7 +250,7 @@ func TestExternalIPAM_PoolFallback(t *testing.T) {
}
func TestExternalIPAM_SpecificPool(t *testing.T) {
pools := []ExternalPoolConfig{
pools := []external.ExternalPoolConfig{
{
Name: "pool-a",
RangeStart: "10.0.0.1",
@@ -276,47 +277,47 @@ func TestExternalIPAM_SpecificPool(t *testing.T) {
func TestExternalIPAM_RangeValidation(t *testing.T) {
tests := []struct {
name string
pool ExternalPoolConfig
pool external.ExternalPoolConfig
wantErr string
}{
{
name: "empty name",
pool: ExternalPoolConfig{RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"},
pool: external.ExternalPoolConfig{RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"},
wantErr: "pool name is required",
},
{
name: "bad range_start",
pool: ExternalPoolConfig{Name: "bad", RangeStart: "not-an-ip", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"},
pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "not-an-ip", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"},
wantErr: "invalid range_start",
},
{
name: "bad range_end",
pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "not-an-ip", Gateway: "10.0.0.254"},
pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "not-an-ip", Gateway: "10.0.0.254"},
wantErr: "invalid range_end",
},
{
name: "start > end",
pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.10", RangeEnd: "10.0.0.1", Gateway: "10.0.0.254"},
pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.10", RangeEnd: "10.0.0.1", Gateway: "10.0.0.254"},
wantErr: "greater than range_end",
},
{
name: "missing gateway",
pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10"},
pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10"},
wantErr: "gateway is required",
},
{
name: "bad gateway",
pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "nope"},
pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "nope"},
wantErr: "invalid gateway IP",
},
{
name: "bad gateway_ip",
pool: ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254", GatewayIP: "nope"},
pool: external.ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254", GatewayIP: "nope"},
wantErr: "invalid gateway_ip",
},
{
name: "valid",
pool: ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"},
pool: external.ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"},
},
}
@@ -338,7 +339,7 @@ func TestExternalIPAM_InitFromConfig(t *testing.T) {
_, _, js := testutil.StartTestJetStream(t)
// First init
ipam1, err := NewExternalIPAM(js, []ExternalPoolConfig{pool})
ipam1, err := NewExternalIPAM(js, []external.ExternalPoolConfig{pool})
require.NoError(t, err)
// Allocate an IP
@@ -347,7 +348,7 @@ func TestExternalIPAM_InitFromConfig(t *testing.T) {
assert.Equal(t, "192.168.1.151", ip)
// Second init (simulating restart) — should not lose allocation
ipam2, err := NewExternalIPAM(js, []ExternalPoolConfig{pool})
ipam2, err := NewExternalIPAM(js, []external.ExternalPoolConfig{pool})
require.NoError(t, err)
record, err := ipam2.GetPoolRecord("wan")
@@ -358,14 +359,14 @@ func TestExternalIPAM_InitFromConfig(t *testing.T) {
func TestExternalIPAM_ReleaseNotAllocated(t *testing.T) {
pool := testPool()
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
err := ipam.ReleaseIP("wan", "192.168.1.200", "")
assert.ErrorContains(t, err, "not allocated")
}
func TestExternalIPAM_NoPoolAvailable(t *testing.T) {
pool := ExternalPoolConfig{
pool := external.ExternalPoolConfig{
Name: "us-only",
RangeStart: "10.0.0.1",
RangeEnd: "10.0.0.10",
@@ -374,7 +375,7 @@ func TestExternalIPAM_NoPoolAvailable(t *testing.T) {
Region: "us-east-1",
AZ: "us-east-1a",
}
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
// No pool matches eu-west
_, _, err := ipam.AllocateIP("eu-west-1", "eu-west-1a", PurposeENIPublic, "", "eni-1", "i-1")
@@ -383,19 +384,19 @@ func TestExternalIPAM_NoPoolAvailable(t *testing.T) {
func TestExternalIPAM_FindPoolByName_NotFound(t *testing.T) {
pool := testPool()
ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool})
ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool})
// AllocateFromPool with a non-existent pool name: the pool "nonexistent"
// has no KV record, so getRecord fails.
kv := ipam.kv
ipam2 := NewExternalIPAMWithKV(kv, []ExternalPoolConfig{pool})
ipam2 := NewExternalIPAMWithKV(kv, []external.ExternalPoolConfig{pool})
_, err := ipam2.AllocateFromPool("nonexistent", PurposeENIPublic, "", "eni-1", "i-1")
assert.Error(t, err)
}
func TestValidatePoolConfig(t *testing.T) {
base := func() ExternalPoolConfig {
return ExternalPoolConfig{
base := func() external.ExternalPoolConfig {
return external.ExternalPoolConfig{
Name: "wan",
Gateway: "192.168.1.1",
RangeStart: "192.168.1.100",
@@ -405,37 +406,37 @@ func TestValidatePoolConfig(t *testing.T) {
}
cases := []struct {
name string
mutate func(*ExternalPoolConfig)
mutate func(*external.ExternalPoolConfig)
wantErr string
}{
{name: "ok", mutate: func(p *ExternalPoolConfig) {}},
{name: "no name", mutate: func(p *ExternalPoolConfig) { p.Name = "" }, wantErr: "pool name is required"},
{name: "no gateway", mutate: func(p *ExternalPoolConfig) { p.Gateway = "" }, wantErr: "gateway is required"},
{name: "bad gateway", mutate: func(p *ExternalPoolConfig) { p.Gateway = "x" }, wantErr: "invalid gateway IP"},
{name: "bad gateway_ip", mutate: func(p *ExternalPoolConfig) { p.GatewayIP = "x" }, wantErr: "invalid gateway_ip"},
{name: "bad range_start", mutate: func(p *ExternalPoolConfig) { p.RangeStart = "x" }, wantErr: "invalid range_start"},
{name: "bad range_end", mutate: func(p *ExternalPoolConfig) { p.RangeEnd = "x" }, wantErr: "invalid range_end"},
{name: "range reversed", mutate: func(p *ExternalPoolConfig) {
{name: "ok", mutate: func(p *external.ExternalPoolConfig) {}},
{name: "no name", mutate: func(p *external.ExternalPoolConfig) { p.Name = "" }, wantErr: "pool name is required"},
{name: "no gateway", mutate: func(p *external.ExternalPoolConfig) { p.Gateway = "" }, wantErr: "gateway is required"},
{name: "bad gateway", mutate: func(p *external.ExternalPoolConfig) { p.Gateway = "x" }, wantErr: "invalid gateway IP"},
{name: "bad gateway_ip", mutate: func(p *external.ExternalPoolConfig) { p.GatewayIP = "x" }, wantErr: "invalid gateway_ip"},
{name: "bad range_start", mutate: func(p *external.ExternalPoolConfig) { p.RangeStart = "x" }, wantErr: "invalid range_start"},
{name: "bad range_end", mutate: func(p *external.ExternalPoolConfig) { p.RangeEnd = "x" }, wantErr: "invalid range_end"},
{name: "range reversed", mutate: func(p *external.ExternalPoolConfig) {
p.RangeStart = "192.168.1.200"
p.RangeEnd = "192.168.1.100"
}, wantErr: "greater than range_end"},
{name: "bad gw_lrp_start", mutate: func(p *ExternalPoolConfig) {
{name: "bad gw_lrp_start", mutate: func(p *external.ExternalPoolConfig) {
p.GwLrpRangeStart = "x"
p.GwLrpRangeEnd = "192.168.1.20"
}, wantErr: "invalid gw_lrp_range_start"},
{name: "bad gw_lrp_end", mutate: func(p *ExternalPoolConfig) {
{name: "bad gw_lrp_end", mutate: func(p *external.ExternalPoolConfig) {
p.GwLrpRangeStart = "192.168.1.20"
p.GwLrpRangeEnd = "x"
}, wantErr: "invalid gw_lrp_range_end"},
{name: "gw_lrp reversed", mutate: func(p *ExternalPoolConfig) {
{name: "gw_lrp reversed", mutate: func(p *external.ExternalPoolConfig) {
p.GwLrpRangeStart = "192.168.1.30"
p.GwLrpRangeEnd = "192.168.1.20"
}, wantErr: "greater than gw_lrp_range_end"},
{name: "gw_lrp overlaps range", mutate: func(p *ExternalPoolConfig) {
{name: "gw_lrp overlaps range", mutate: func(p *external.ExternalPoolConfig) {
p.GwLrpRangeStart = "192.168.1.105"
p.GwLrpRangeEnd = "192.168.1.108"
}, wantErr: "overlaps range"},
{name: "gw_lrp valid below range", mutate: func(p *ExternalPoolConfig) {
{name: "gw_lrp valid below range", mutate: func(p *external.ExternalPoolConfig) {
p.GwLrpRangeStart = "192.168.1.20"
p.GwLrpRangeEnd = "192.168.1.29"
}},
+13 -17
View File
@@ -1,11 +1,11 @@
package handlers_ec2_vpc
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big"
"net"
"github.com/mulgadc/spinifex/spinifex/migrate"
@@ -188,16 +188,14 @@ func (m *IPAM) nextAvailableIP(record *IPAMRecord) (string, error) {
}
ones, bits := ipNet.Mask.Size()
hostBits := bits - ones // always 0-32 for IPv4 CIDRs, safe for uint conversion
totalIPs := new(big.Int).Lsh(big.NewInt(1), uint(hostBits)) //#nosec G115 - hostBits is 0-32 for IPv4
hostBits := bits - ones // 0-32 for IPv4 CIDRs
totalIPs := uint64(1) << hostBits // fits uint64 (max 2^32)
// Start at offset 4 (.0=network, .1=gateway, .2=DNS, .3=reserved)
networkIP := ipToInt(ipNet.IP)
for offset := int64(4); offset < totalIPs.Int64()-1; offset++ { // -1 for broadcast
candidateInt := new(big.Int).Add(networkIP, big.NewInt(offset))
candidate := intToIP(candidateInt).String()
for offset := uint64(4); offset < totalIPs-1; offset++ { // -1 for broadcast
candidate := intToIP(networkIP + uint32(offset)).String()
if !allocated[candidate] {
return candidate, nil
}
@@ -206,20 +204,18 @@ func (m *IPAM) nextAvailableIP(record *IPAMRecord) (string, error) {
return "", fmt.Errorf("subnet %s exhausted, no IPs available", record.CidrBlock)
}
// ipToInt converts a net.IP to a big.Int.
func ipToInt(ip net.IP) *big.Int {
// ipToInt converts an IPv4 net.IP to its uint32 representation.
func ipToInt(ip net.IP) uint32 {
ip = ip.To4()
if ip == nil {
return big.NewInt(0)
return 0
}
return new(big.Int).SetBytes(ip)
return binary.BigEndian.Uint32(ip)
}
// intToIP converts a big.Int back to a net.IP (IPv4).
func intToIP(n *big.Int) net.IP {
b := n.Bytes()
// Pad to 4 bytes for IPv4
ip := make(net.IP, 4)
copy(ip[4-len(b):], b)
// intToIP converts a uint32 back to an IPv4 net.IP.
func intToIP(n uint32) net.IP {
ip := make(net.IP, net.IPv4len)
binary.BigEndian.PutUint32(ip, n)
return ip
}
+1 -2
View File
@@ -192,8 +192,7 @@ func TestIPAM_InvalidCIDR(t *testing.T) {
}
func TestIPAM_IpToIntNil(t *testing.T) {
result := ipToInt(nil)
assert.Equal(t, int64(0), result.Int64())
assert.Equal(t, uint32(0), ipToInt(nil))
}
func itoa(i int) string {
+3 -39
View File
@@ -103,7 +103,7 @@ type Config struct {
// ExternalMode is "pool" or "" (disabled).
ExternalMode string
// ExternalPools holds the cluster-wide external IP pool configs.
ExternalPools []ExternalPoolConfig
ExternalPools []external.ExternalPoolConfig
// Bootstrap holds the default VPC config from spinifex.toml for first-boot reconciliation.
Bootstrap *BootstrapVPC
// ExternalInterface is the WAN NIC name (e.g., "enp0s3"). Used by
@@ -119,23 +119,6 @@ type Config struct {
AZ string
}
// ExternalPoolConfig mirrors config.ExternalPool for vpcd's internal use.
type ExternalPoolConfig struct {
Name string
Source string // "static" (default) or "dhcp"
BindBridge string // Linux bridge for DHCP DORA (source=dhcp only)
RangeStart string
RangeEnd string
Gateway string
GatewayIP string
PrefixLen int
DNSServers []string
Region string
AZ string
GwLrpRangeStart string // Sub-range for OVN gateway LRP IPs in centralized NAT mode.
GwLrpRangeEnd string
}
// Service implements the Spinifex service interface for vpcd.
type Service struct {
Config *Config
@@ -450,7 +433,7 @@ func launchService(cfg *Config) error {
var igwPool *external.ExternalPoolConfig
if cfg.ExternalMode != "" && len(cfg.ExternalPools) > 0 {
p := externalPoolConfigToShared(cfg.ExternalPools[0])
p := cfg.ExternalPools[0]
igwPool = &p
}
@@ -608,7 +591,7 @@ func launchService(cfg *Config) error {
// pickDNSServer returns the OVN dhcp_options dns_server from the first unscoped pool with DNS servers.
// Empty falls back to topology.NewLiveManager's default.
func pickDNSServer(pools []ExternalPoolConfig) string {
func pickDNSServer(pools []external.ExternalPoolConfig) string {
for _, p := range pools {
if p.Region == "" && p.AZ == "" && len(p.DNSServers) > 0 {
return "{" + strings.Join(p.DNSServers, ", ") + "}"
@@ -665,25 +648,6 @@ func pickGatewayAllocator(pool *external.ExternalPoolConfig, ovnClient ovn.Clien
return external.NewStaticRangeAllocator(ovnClient)
}
// externalPoolConfigToShared translates vpcd's ExternalPoolConfig into the network/external shared type.
func externalPoolConfigToShared(p ExternalPoolConfig) external.ExternalPoolConfig {
return external.ExternalPoolConfig{
Name: p.Name,
Source: p.Source,
BindBridge: p.BindBridge,
RangeStart: p.RangeStart,
RangeEnd: p.RangeEnd,
Gateway: p.Gateway,
GatewayIP: p.GatewayIP,
PrefixLen: p.PrefixLen,
DNSServers: p.DNSServers,
Region: p.Region,
AZ: p.AZ,
GwLrpRangeStart: p.GwLrpRangeStart,
GwLrpRangeEnd: p.GwLrpRangeEnd,
}
}
// resolveBridgeConfig picks bridge mode (auto-detecting when unset) and always uses "br-wan" as the WAN bridge.
func resolveBridgeConfig(cfgBridgeMode, externalIface string) (string, string) {
bridgeMode := cfgBridgeMode