Skip to content

Commit

Permalink
Support NSX-T Edge Gatway IP count limits (vmware#682)
Browse files Browse the repository at this point in the history
  • Loading branch information
Didainius committed Jun 19, 2024
1 parent c6d395d commit 3167110
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changes/v2.25.0/682-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Added method `NsxtEdgeGateway.GetUsedAndUnusedExternalIPAddressCountWithLimit` to count used
and unused IPs assigned to Edge Gateway. It supports a `limitTo` argument that can prevent
exhausting system resources when counting IPs in assigned subnets [GH-682]
61 changes: 56 additions & 5 deletions govcd/nsxt_edgegateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"net/netip"
"net/url"
"time"

"github.com/vmware/go-vcloud-director/v2/types/v56"
"github.com/vmware/go-vcloud-director/v2/util"
Expand Down Expand Up @@ -512,6 +513,9 @@ func (egw *NsxtEdgeGateway) GetUnusedExternalIPAddresses(requiredIpCount int, op

// GetAllUnusedExternalIPAddresses will retrieve all unassigned IP addresses for Edge Gateway It is
// similar to GetUnusedExternalIPAddresses but returns all unused IPs instead of a specific amount
//
// Note. In case a very large subnet of IPv6 is present this function might exhaust memory. Please
// use GetUnusedExternalIPAddressesWithCountLimit in such cases
func (egw *NsxtEdgeGateway) GetAllUnusedExternalIPAddresses(refresh bool) ([]netip.Addr, error) {
if refresh {
err := egw.Refresh()
Expand All @@ -524,7 +528,35 @@ func (egw *NsxtEdgeGateway) GetAllUnusedExternalIPAddresses(refresh bool) ([]net
return nil, fmt.Errorf("error getting used IP addresses for Edge Gateway: %s", err)
}

return getAllUnusedExternalIPAddresses(egw.EdgeGateway.EdgeGatewayUplinks, usedIpAddresses, netip.Prefix{})
return getAllUnusedExternalIPAddresses(egw.EdgeGateway.EdgeGatewayUplinks, usedIpAddresses, netip.Prefix{}, 0)
}

// GetUsedAndUnusedExternalIPAddressCountWithLimit will count IPs and can limit their total count up
// to 'limitTo' which can be used to count IPs with huge IPv6 subnets
//
// Return order - usedIpCount, unusedIpCount, error
func (egw *NsxtEdgeGateway) GetUsedAndUnusedExternalIPAddressCountWithLimit(refresh bool, limitTo int64) (int64, int64, error) {
if refresh {
err := egw.Refresh()
if err != nil {
return 0, 0, fmt.Errorf("error refreshing Edge Gateway: %s", err)
}
}
usedIpAddresses, err := egw.GetUsedIpAddresses(nil)
if err != nil {
return 0, 0, fmt.Errorf("error getting used IP addresses for Edge Gateway: %s", err)
}

assignedIpAddresses, err := flattenEdgeGatewayUplinkToIpSlice(egw.EdgeGateway.EdgeGatewayUplinks, limitTo)
if err != nil {
return 0, 0, fmt.Errorf("error listing all IPs in Edge Gateway: %s", err)
}

usedIpCount := int64(len(usedIpAddresses))
assignedIpCount := int64(len(assignedIpAddresses))
unusedIpCount := assignedIpCount - usedIpCount

return usedIpCount, unusedIpCount, nil
}

// GetAllocatedIpCount traverses all subnets in Edge Gateway and returns a count of allocated IP
Expand Down Expand Up @@ -890,11 +922,11 @@ func (egw *NsxtEdgeGateway) UpdateSlaacProfile(slaacProfileConfig *types.NsxtEdg
return updatedSlaacProfile, nil
}

func getAllUnusedExternalIPAddresses(uplinks []types.EdgeGatewayUplinks, usedIpAddresses []*types.GatewayUsedIpAddress, optionalSubnet netip.Prefix) ([]netip.Addr, error) {
func getAllUnusedExternalIPAddresses(uplinks []types.EdgeGatewayUplinks, usedIpAddresses []*types.GatewayUsedIpAddress, optionalSubnet netip.Prefix, limitTo int64) ([]netip.Addr, error) {
// 1. Flatten all IP ranges in Edge Gateway using Go's native 'netip.Addr' IP container instead
// of plain strings because it is more robust (supports IPv4 and IPv6 and also comparison
// operator)
assignedIpSlice, err := flattenEdgeGatewayUplinkToIpSlice(uplinks)
assignedIpSlice, err := flattenEdgeGatewayUplinkToIpSlice(uplinks, limitTo)
if err != nil {
return nil, fmt.Errorf("error listing all IPs in Edge Gateway: %s", err)
}
Expand Down Expand Up @@ -925,7 +957,7 @@ func getAllUnusedExternalIPAddresses(uplinks []types.EdgeGatewayUplinks, usedIpA
}

func getUnusedExternalIPAddress(uplinks []types.EdgeGatewayUplinks, usedIpAddresses []*types.GatewayUsedIpAddress, requiredIpCount int, optionalSubnet netip.Prefix) ([]netip.Addr, error) {
unusedIps, err := getAllUnusedExternalIPAddresses(uplinks, usedIpAddresses, optionalSubnet)
unusedIps, err := getAllUnusedExternalIPAddresses(uplinks, usedIpAddresses, optionalSubnet, 0)
if err != nil {
return nil, fmt.Errorf("error getting all unused IPs: %s", err)
}
Expand All @@ -941,9 +973,17 @@ func getUnusedExternalIPAddress(uplinks []types.EdgeGatewayUplinks, usedIpAddres

// flattenEdgeGatewayUplinkToIpSlice processes Edge Gateway Uplink structure and creates a slice of
// all available IPs
func flattenEdgeGatewayUplinkToIpSlice(uplinks []types.EdgeGatewayUplinks) ([]netip.Addr, error) {
// Note. Having a huge IPv6 block might become a long running task and potentially exhaust system
// memory. One can use 'limitTo' setting to set upper limit for number of IPs that one wants to
// retrieve. Setting `limitTo` to 0 means that not limitation is applied.
func flattenEdgeGatewayUplinkToIpSlice(uplinks []types.EdgeGatewayUplinks, limitTo int64) ([]netip.Addr, error) {
start := time.Now()
util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice starting at %s with limitTo %d", start.String(), limitTo)
util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice Edge Gateway uplink count %d", len(uplinks))
assignedIpSlice := make([]netip.Addr, 0)

var counter int64

for _, edgeGatewayUplink := range uplinks {
for _, edgeGatewayUplinkSubnet := range edgeGatewayUplink.Subnets.Values {
for _, r := range edgeGatewayUplinkSubnet.IPRanges.Values {
Expand All @@ -970,13 +1010,24 @@ func flattenEdgeGatewayUplinkToIpSlice(uplinks []types.EdgeGatewayUplinks) ([]ne
// Expression 'ip.Compare(endIp) == 1' means that 'ip > endIp' and the loop should stop
for ip := startIp; ip.Compare(endIp) != 1; ip = ip.Next() {
assignedIpSlice = append(assignedIpSlice, ip)
counter++
if limitTo != 0 && counter >= limitTo {
util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice hit limitTo %d at %s with IP range", limitTo, time.Since(start))
return assignedIpSlice, nil
}
}
} else { // if there is no end address in the range, then it is only a single IP - startIp
assignedIpSlice = append(assignedIpSlice, startIp)
counter++
if limitTo != 0 && counter >= limitTo {
util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice hit limitTo %d at %s with single IP", limitTo, time.Since(start))
return assignedIpSlice, nil
}
}
}
}
}
util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice finished %s", time.Since(start))

return assignedIpSlice, nil
}
Expand Down
6 changes: 6 additions & 0 deletions govcd/nsxt_edgegateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,12 @@ func (vcd *TestVCD) Test_NsxtEdgeGatewayUsedAndUnusedIPs(check *C) {
check.Assert(ipsCompared, Equals, true)
check.Assert(len(allIps), Equals, 22)

// Get used and unused IP counts
usedIpCount, unusedIpCount, err := createdEdge.GetUsedAndUnusedExternalIPAddressCountWithLimit(false, 5)
check.Assert(err, IsNil)
check.Assert(unusedIpCount, Equals, int64(4))
check.Assert(usedIpCount, Equals, int64(1))

// Verify that GetAllocatedIpCount returns correct number of allocated IPs
totalAllocationIpCount, err := createdEdge.GetAllocatedIpCount(true)
check.Assert(err, IsNil)
Expand Down
61 changes: 60 additions & 1 deletion govcd/nsxt_edgegateway_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ func Test_ipSliceDifference(t *testing.T) {
func Test_flattenEdgeGatewayUplinkToIpSlice(t *testing.T) {
type args struct {
uplinks []types.EdgeGatewayUplinks
limitTo int64
}
tests := []struct {
name string
Expand Down Expand Up @@ -402,6 +403,64 @@ func Test_flattenEdgeGatewayUplinkToIpSlice(t *testing.T) {
},
wantErr: false,
},
{
name: "IPv6BigSubnetLimit3",
args: args{
uplinks: []types.EdgeGatewayUplinks{
{
Subnets: types.OpenAPIEdgeGatewaySubnets{
Values: []types.OpenAPIEdgeGatewaySubnetValue{
{
IPRanges: &types.OpenApiIPRanges{
Values: []types.OpenApiIPRangeValues{
{
StartAddress: "2a02:a404:11:0:0:0:0:1",
EndAddress: "2a02:a404:11:0:ffff:ffff:ffff:fffd",
},
},
},
},
},
},
},
},
limitTo: 3,
},
want: []netip.Addr{
netip.MustParseAddr("2a02:a404:11:0:0:0:0:1"),
netip.MustParseAddr("2a02:a404:11:0:0:0:0:2"),
netip.MustParseAddr("2a02:a404:11:0:0:0:0:3"),
},
wantErr: false,
},
{
name: "IPv6BigSubnetLimit1",
args: args{
uplinks: []types.EdgeGatewayUplinks{
{
Subnets: types.OpenAPIEdgeGatewaySubnets{
Values: []types.OpenAPIEdgeGatewaySubnetValue{
{
IPRanges: &types.OpenApiIPRanges{
Values: []types.OpenApiIPRangeValues{
{
StartAddress: "2a02:a404:11:0:0:0:0:1",
EndAddress: "2a02:a404:11:0:ffff:ffff:ffff:fffd",
},
},
},
},
},
},
},
},
limitTo: 1,
},
want: []netip.Addr{
netip.MustParseAddr("2a02:a404:11:0:0:0:0:1"),
},
wantErr: false,
},
{
name: "ReverseStartAndEnd",
args: args{
Expand Down Expand Up @@ -557,7 +616,7 @@ func Test_flattenEdgeGatewayUplinkToIpSlice(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := flattenEdgeGatewayUplinkToIpSlice(tt.args.uplinks)
got, err := flattenEdgeGatewayUplinkToIpSlice(tt.args.uplinks, tt.args.limitTo)
if (err != nil) != tt.wantErr {
t.Errorf("ipSliceFromEdgeGatewayUplinks() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down

0 comments on commit 3167110

Please sign in to comment.