From a5bd65bcff6c779189723354c5366cb7bfe0e76a Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Tue, 9 Jun 2026 15:58:47 -0700 Subject: [PATCH 1/7] fix(rest-api): Check IP Usage for requested Prefix before creating or updating Instance --- rest-api/api/pkg/api/handler/instance.go | 42 ++++++++ rest-api/api/pkg/api/handler/instance_test.go | 96 ++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index 16852d0876..852e988c93 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -434,6 +434,17 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } for i := range subnets { subnetIDMap[subnets[i].ID] = &subnets[i] + + // Check if subnet prefix is exhausted + subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, &subnets[i]) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for Subnet") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) + } + if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnets[i].ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnets[i].ID), nil) + } } } @@ -447,6 +458,17 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } for i := range vpcPrefixes { vpcPrefixIDMap[vpcPrefixes[i].ID] = &vpcPrefixes[i] + + // Check if VPC Prefix is exhausted + vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, &vpcPrefixes[i]) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) + } + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixes[i].ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixes[i].ID), nil) + } } } @@ -2291,6 +2313,16 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Subnets from DB by IDs", nil) } for i := range subnetList { + // Check if Subnet is exhausted + subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, &subnetList[i]) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for Subnet") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) + } + if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetList[i].ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetList[i].ID), nil) + } subnetIDMap[subnetList[i].ID] = &subnetList[i] } } @@ -2304,6 +2336,16 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve VPC Prefixes from DB by IDs", nil) } for i := range vpcPrefixList { + // Check if VPC Prefix is exhausted + vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, &vpcPrefixList[i]) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) + } + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixList[i].ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixList[i].ID), nil) + } vpcPrefixIDMap[vpcPrefixList[i].ID] = &vpcPrefixList[i] } } diff --git a/rest-api/api/pkg/api/handler/instance_test.go b/rest-api/api/pkg/api/handler/instance_test.go index 2671f354dc..8ec8e6844e 100644 --- a/rest-api/api/pkg/api/handler/instance_test.go +++ b/rest-api/api/pkg/api/handler/instance_test.go @@ -279,6 +279,7 @@ func testInstanceBuildVPC(t *testing.T, dbSession *cdb.Session, name string, ip func testInstanceBuildSubnet(t *testing.T, dbSession *cdb.Session, name string, tn *cdbm.Tenant, vpc *cdbm.Vpc, cnsID *uuid.UUID, status string, user *cdbm.User) *cdbm.Subnet { subnetDAO := cdbm.NewSubnetDAO(dbSession) + ipv4Prefix := fmt.Sprintf("10.%d.0.0/24", (int(name[0])+len(name))%200+1) subnet, err := subnetDAO.Create(context.Background(), nil, cdbm.SubnetCreateInput{ Name: name, @@ -288,7 +289,8 @@ func testInstanceBuildSubnet(t *testing.T, dbSession *cdb.Session, name string, VpcID: vpc.ID, TenantID: tn.ID, ControllerNetworkSegmentID: cnsID, - PrefixLength: 0, + IPv4Prefix: &ipv4Prefix, + PrefixLength: 24, Status: status, CreatedBy: user.ID, }) @@ -772,6 +774,11 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { alc1 := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, ist1.ID, cdbm.AllocationConstraintTypeReserved, 9, ipu) assert.NotNil(t, alc1) + // Dedicated instance type for IP-exhaustion fixtures; must not consume ist1 allocation (limit 9). + istExhaustFixture := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-exhaust-fixture", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istExhaustFixture) + testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istExhaustFixture.ID, cdbm.AllocationConstraintTypeReserved, 30, ipu) + mc1 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cutil.GetPtr(false), nil) assert.NotNil(t, mc1) @@ -884,6 +891,27 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { subnetPending := testInstanceBuildSubnet(t, dbSession, "test-subnet-5", tn1, vpcSiteReady, nil, cdbm.SubnetStatusPending, tnu1) assert.NotNil(t, subnetPending) + subnetExhaustedIPv4 := "10.99.0.0/28" + subnetExhausted, err := cdbm.NewSubnetDAO(dbSession).Create(context.Background(), nil, cdbm.SubnetCreateInput{ + Name: "test-subnet-exhausted", + Description: cutil.GetPtr("Test Subnet exhausted"), + Org: tn1.Org, + SiteID: vpc1.SiteID, + VpcID: vpc1.ID, + TenantID: tn1.ID, + ControllerNetworkSegmentID: cutil.GetPtr(uuid.New()), + IPv4Prefix: &subnetExhaustedIPv4, + PrefixLength: 28, + Status: cdbm.SubnetStatusReady, + CreatedBy: tnu1.ID, + }) + assert.Nil(t, err) + assert.NotNil(t, subnetExhausted) + for i := 0; i < 14; i++ { + exhaustInst := testInstanceBuildInstance(t, dbSession, fmt.Sprintf("exhaust-subnet-inst-%d", i), tn1.ID, ip.ID, st1.ID, &istExhaustFixture.ID, vpc1.ID, nil, &os1.ID, nil, cdbm.InstanceStatusReady) + testInstanceBuildInstanceInterface(t, dbSession, exhaustInst.ID, &subnetExhausted.ID, nil, nil, cdbm.InterfaceStatusPending) + } + mci1 := testInstanceBuildMachineInterface(t, dbSession, subnet1.ID, mc1.ID) assert.NotNil(t, mci1) @@ -1148,6 +1176,14 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { vpcPrefix7 := common.TestBuildVPCPrefix(t, dbSession, "test-vpcprefix-7", st1, tn1, vpc9.ID, &ipb5.ID, cutil.GetPtr("192.168.0.0/24"), cutil.GetPtr(24), cdbm.VpcPrefixStatusReady, tnu1) vpcPrefixSite2 := common.TestBuildVPCPrefix(t, dbSession, "test-vpcprefix-site2", st2, tn1, vpc9Site2.ID, &ipbSite2.ID, cutil.GetPtr("192.170.0.0/24"), cutil.GetPtr(24), cdbm.VpcPrefixStatusReady, tnu1) assert.NotNil(t, vpcPrefixSite2) + ipbExhausted := common.TestBuildVpcPrefixIPBlock(t, dbSession, "testipb-exhausted", st1, ip, &tn1.ID, cdbm.IPBlockRoutingTypeDatacenterOnly, "10.99.1.0", 28, cdbm.IPBlockProtocolVersionV4, false, cdbm.IPBlockStatusReady, tnu1) + assert.NotNil(t, ipbExhausted) + vpcPrefixExhausted := common.TestBuildVPCPrefix(t, dbSession, "test-vpcprefix-exhausted", st1, tn1, vpc9.ID, &ipbExhausted.ID, cutil.GetPtr("10.99.1.0/28"), cutil.GetPtr(28), cdbm.VpcPrefixStatusReady, tnu1) + assert.NotNil(t, vpcPrefixExhausted) + for i := 0; i < 8; i++ { + exhaustInst := testInstanceBuildInstance(t, dbSession, fmt.Sprintf("exhaust-vpcprefix-inst-%d", i), tn1.ID, ip.ID, st1.ID, &istExhaustFixture.ID, vpc9.ID, nil, &os1.ID, nil, cdbm.InstanceStatusReady) + testInstanceBuildInstanceInterface(t, dbSession, exhaustInst.ID, nil, &vpcPrefixExhausted.ID, nil, cdbm.InterfaceStatusPending) + } // NvLink Logical Partition nvllp1 := testBuildNVLinkLogicalPartition(t, dbSession, "test-nvllp-1", cutil.GetPtr("Test NVLink Logical Partition"), tnOrg, st1, tn1, cutil.GetPtr(cdbm.NVLinkLogicalPartitionStatusReady), false) assert.NotNil(t, nvllp1) @@ -3425,6 +3461,64 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { wantErr: false, verifyChildSpanner: true, }, + { + name: "test Instance create API endpoint failed when subnet IP addresses are exhausted", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceCreateRequest{ + Name: "Test Instance subnet exhausted", + TenantID: tn1.ID.String(), + InstanceTypeID: cutil.GetPtr(ist1.ID.String()), + VpcID: vpc1.ID.String(), + OperatingSystemID: cutil.GetPtr(os1.ID.String()), + IpxeScript: cutil.GetPtr(common.DefaultIpxeScript), + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{ + { + SubnetID: cutil.GetPtr(subnetExhausted.ID.String()), + }, + }, + }, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusBadRequest, + respMessage: fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetExhausted.ID), + }, + wantErr: false, + }, + { + name: "test Instance create API endpoint failed when VPC Prefix IP addresses are exhausted", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceCreateRequest{ + Name: "Test Instance vpc prefix exhausted", + TenantID: tn1.ID.String(), + InstanceTypeID: cutil.GetPtr(ist1.ID.String()), + VpcID: vpc9.ID.String(), + OperatingSystemID: cutil.GetPtr(os1.ID.String()), + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{ + { + VpcPrefixID: cutil.GetPtr(vpcPrefixExhausted.ID.String()), + IsPhysical: true, + Device: cutil.GetPtr("MT42822 BlueField-2 integrated ConnectX-6 Dx network controller"), + DeviceInstance: cutil.GetPtr(0), + }, + }, + }, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusBadRequest, + respMessage: fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixExhausted.ID), + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 783899b1708114c5aeb4d7417f2d4089ac941028 Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Tue, 9 Jun 2026 16:45:17 -0700 Subject: [PATCH 2/7] Moved the check where Prefix for each Interface validate --- rest-api/api/pkg/api/handler/instance.go | 86 ++++++++++--------- rest-api/api/pkg/api/handler/instance_test.go | 3 +- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index 852e988c93..b71a22ac4b 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -434,17 +434,6 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } for i := range subnets { subnetIDMap[subnets[i].ID] = &subnets[i] - - // Check if subnet prefix is exhausted - subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, &subnets[i]) - if err != nil { - logger.Error().Err(err).Msg("error getting prefix usage for Subnet") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) - } - if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnets[i].ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnets[i].ID), nil) - } } } @@ -458,17 +447,6 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } for i := range vpcPrefixes { vpcPrefixIDMap[vpcPrefixes[i].ID] = &vpcPrefixes[i] - - // Check if VPC Prefix is exhausted - vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, &vpcPrefixes[i]) - if err != nil { - logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) - } - if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixes[i].ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixes[i].ID), nil) - } } } @@ -531,6 +509,17 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have Ethernet network virtualization type in order to create Subnet based interfaces", vpc.ID), nil) } + // Check if subnet prefix is exhausted + subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, subnet) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for Subnet") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) + } + if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID), nil) + } + dbInterfaces = append(dbInterfaces, cdbm.Interface{ SubnetID: &subnetID, IsPhysical: ifc.IsPhysical, @@ -602,6 +591,17 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have FNN network virtualization type in order to create VPC Prefix based interfaces", vpc.ID), nil) } + // Check if VPC Prefix is exhausted + vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) + } + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID), nil) + } + dbInterfaces = append(dbInterfaces, cdbm.Interface{ VpcPrefixID: &vpcPrefixID, VpcPrefix: vpcPrefix, // We attach this here so it can be used when we convert to the API model. @@ -2313,16 +2313,6 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Subnets from DB by IDs", nil) } for i := range subnetList { - // Check if Subnet is exhausted - subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, &subnetList[i]) - if err != nil { - logger.Error().Err(err).Msg("error getting prefix usage for Subnet") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) - } - if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetList[i].ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetList[i].ID), nil) - } subnetIDMap[subnetList[i].ID] = &subnetList[i] } } @@ -2336,16 +2326,6 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve VPC Prefixes from DB by IDs", nil) } for i := range vpcPrefixList { - // Check if VPC Prefix is exhausted - vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, &vpcPrefixList[i]) - if err != nil { - logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) - } - if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixList[i].ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixList[i].ID), nil) - } vpcPrefixIDMap[vpcPrefixList[i].ID] = &vpcPrefixList[i] } } @@ -2408,6 +2388,17 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have Ethernet network virtualization type in order to create Subnet based interfaces", instance.VpcID), nil) } + // Check if Subnet is exhausted + subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, subnet) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for Subnet") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) + } + if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID), nil) + } + dbInterfaces = append(dbInterfaces, cdbm.Interface{ SubnetID: &subnetID, IsPhysical: ifc.IsPhysical, @@ -2478,6 +2469,17 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have FNN network virtualization type in order to create VPC Prefix based interfaces", instance.VpcID), nil) } + // Check if VPC Prefix is exhausted + vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) + if err != nil { + logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) + } + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID), nil) + } + dbInterfaces = append(dbInterfaces, cdbm.Interface{ VpcPrefixID: &vpcPrefixID, VpcPrefix: vpcPrefix, // We attach this here so it can be used when we convert to the API model. diff --git a/rest-api/api/pkg/api/handler/instance_test.go b/rest-api/api/pkg/api/handler/instance_test.go index 8ec8e6844e..3de51ada22 100644 --- a/rest-api/api/pkg/api/handler/instance_test.go +++ b/rest-api/api/pkg/api/handler/instance_test.go @@ -777,7 +777,8 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { // Dedicated instance type for IP-exhaustion fixtures; must not consume ist1 allocation (limit 9). istExhaustFixture := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-exhaust-fixture", st1, cdbm.InstanceStatusReady) assert.NotNil(t, istExhaustFixture) - testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istExhaustFixture.ID, cdbm.AllocationConstraintTypeReserved, 30, ipu) + alcExhaustFixture := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istExhaustFixture.ID, cdbm.AllocationConstraintTypeReserved, 30, ipu) + assert.NotNil(t, alcExhaustFixture) mc1 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cutil.GetPtr(false), nil) assert.NotNil(t, mc1) From a0300ef08ac0f2c8e5d02215ec9000283218b4ab Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Wed, 10 Jun 2026 12:52:24 -0700 Subject: [PATCH 3/7] Revised usage calculation for IP exhaust --- rest-api/api/pkg/api/handler/instance.go | 75 +++++++++++++++++------- 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index b71a22ac4b..9b0364f026 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -404,6 +404,8 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { subnetIDs := []uuid.UUID{} vpcPrefixIDs := []uuid.UUID{} + subnetIfcMap := map[uuid.UUID]int{} + vpcPrefixIfcMap := map[uuid.UUID]int{} for _, ifc := range apiRequest.Interfaces { if ifc.SubnetID != nil { @@ -413,6 +415,7 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Subnet ID: %s specified in interfaces data in request is not valid", *ifc.SubnetID), nil) } subnetIDs = append(subnetIDs, subnetID) + subnetIfcMap[subnetID]++ } if ifc.VpcPrefixID != nil { vpcPrefixID, err := uuid.Parse(*ifc.VpcPrefixID) @@ -421,6 +424,7 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC Prefix ID: %s specified in interfaces data in request is not valid", *ifc.VpcPrefixID), nil) } vpcPrefixIDs = append(vpcPrefixIDs, vpcPrefixID) + vpcPrefixIfcMap[vpcPrefixID]++ } } @@ -509,15 +513,16 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have Ethernet network virtualization type in order to create Subnet based interfaces", vpc.ID), nil) } - // Check if subnet prefix is exhausted + // Check if Subnet is exhausted + incomingInterfaceIPs := subnetIfcMap[subnetID] subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, subnet) - if err != nil { + if err != nil || subnetUsage == nil { + // Just log the error and continue logger.Error().Err(err).Msg("error getting prefix usage for Subnet") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) } - if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID), nil) + if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > subnetUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID), nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ @@ -592,14 +597,15 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } // Check if VPC Prefix is exhausted + incomingInterfaceIPs := vpcPrefixIfcMap[vpcPrefixID] vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) - if err != nil { + if err != nil || vpUsage == nil { + // Just log the error and continue logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) } - if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID), nil) + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > vpUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID), nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ @@ -2284,6 +2290,8 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { // Collect all Subnet and VPC Prefix IDs for batch query subnetIDs := []uuid.UUID{} vpcPrefixIDs := []uuid.UUID{} + subnetIfcMap := map[uuid.UUID]int{} + vpcPrefixIfcMap := map[uuid.UUID]int{} for _, ifc := range apiRequest.Interfaces { if ifc.SubnetID != nil { @@ -2293,6 +2301,7 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Subnet ID specified in request data is not valid", nil) } subnetIDs = append(subnetIDs, subnetID) + subnetIfcMap[subnetID]++ } if ifc.VpcPrefixID != nil { vpcPrefixID, err := uuid.Parse(*ifc.VpcPrefixID) @@ -2301,6 +2310,7 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "VPC Prefix ID specified in request data is not valid", nil) } vpcPrefixIDs = append(vpcPrefixIDs, vpcPrefixID) + vpcPrefixIfcMap[vpcPrefixID]++ } } @@ -2330,6 +2340,26 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { } } + existingSubnetIfcMap := map[uuid.UUID]int{} + existingVpcPrefixIfcMap := map[uuid.UUID]int{} + if len(apiRequest.Interfaces) > 0 { + ifcDAO := cdbm.NewInterfaceDAO(uih.dbSession) + existingIfcsForCapacity, _, err := ifcDAO.GetAll(ctx, nil, cdbm.InterfaceFilterInput{InstanceIDs: []uuid.UUID{instance.ID}}, cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) + if err != nil { + logger.Error().Err(err).Msg("error retrieving existing Interfaces for Instance") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve existing Interfaces for Instance", nil) + } + for i := range existingIfcsForCapacity { + eifc := &existingIfcsForCapacity[i] + if eifc.SubnetID != nil { + existingSubnetIfcMap[*eifc.SubnetID]++ + } + if eifc.VpcPrefixID != nil { + existingVpcPrefixIfcMap[*eifc.VpcPrefixID]++ + } + } + } + // Validate each Interface against fetched data dbInterfaces := []cdbm.Interface{} isDeviceInfoPresent := false @@ -2388,15 +2418,15 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have Ethernet network virtualization type in order to create Subnet based interfaces", instance.VpcID), nil) } - // Check if Subnet is exhausted + incomingInterfaceIPs := subnetIfcMap[subnetID] - existingSubnetIfcMap[subnetID] subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, subnet) if err != nil { + // Just log the error and continue logger.Error().Err(err).Msg("error getting prefix usage for Subnet") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) } - if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs >= subnetUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnet.ID), nil) + if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > subnetUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID), nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ @@ -2469,15 +2499,16 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have FNN network virtualization type in order to create VPC Prefix based interfaces", instance.VpcID), nil) } - // Check if VPC Prefix is exhausted + incomingInterfaceIPs := vpcPrefixIfcMap[vpcPrefixID] - existingVpcPrefixIfcMap[vpcPrefixID] + vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) - if err != nil { + if err != nil || vpUsage == nil { + // Just log the error and continue logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) } - if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs >= vpUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefix.ID), nil) + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs)*2 > vpUsage.AvailableIPs { + logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID)) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID), nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ From 22f5c642f508e81eda18953eee847a89e70a0373 Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Wed, 10 Jun 2026 13:42:42 -0700 Subject: [PATCH 4/7] Optimized Usage for handling multiple Subnet and VpcPrefix --- rest-api/api/pkg/api/handler/instance.go | 77 ++++++++++---- rest-api/api/pkg/api/handler/subnet.go | 22 +++- rest-api/api/pkg/api/handler/vpcprefix.go | 22 +++- rest-api/db/pkg/db/model/subnet.go | 113 +++++++++++++------- rest-api/db/pkg/db/model/vpcprefix.go | 121 ++++++++++++++-------- 5 files changed, 244 insertions(+), 111 deletions(-) diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index 9b0364f026..1dfc3bb65f 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -483,6 +483,30 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Primary VPC ID: %s for Instance must not be listed in `secondaryVpcIds`", vpc.ID), nil) } + subnetsForUsage := make([]*cdbm.Subnet, 0, len(subnetIfcMap)) + for subnetID := range subnetIfcMap { + if sn, ok := subnetIDMap[subnetID]; ok { + subnetsForUsage = append(subnetsForUsage, sn) + } + } + subnetUsageMap, usageErr := sbDAO.GetPrefixUsage(ctx, nil, subnetsForUsage...) + if usageErr != nil { + logger.Error().Err(usageErr).Msg("error getting prefix usage for Subnets") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) + } + + vpcPrefixesForUsage := make([]*cdbm.VpcPrefix, 0, len(vpcPrefixIfcMap)) + for vpcPrefixID := range vpcPrefixIfcMap { + if vp, ok := vpcPrefixIDMap[vpcPrefixID]; ok { + vpcPrefixesForUsage = append(vpcPrefixesForUsage, vp) + } + } + vpcPrefixUsageMap, vpUsageErr := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefixesForUsage...) + if vpUsageErr != nil { + logger.Error().Err(vpUsageErr).Msg("error getting prefix usage for VPC Prefixes") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) + } + for _, ifc := range apiRequest.Interfaces { if ifc.SubnetID != nil { subnetID := uuid.MustParse(*ifc.SubnetID) @@ -515,11 +539,7 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { // Check if Subnet is exhausted incomingInterfaceIPs := subnetIfcMap[subnetID] - subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, subnet) - if err != nil || subnetUsage == nil { - // Just log the error and continue - logger.Error().Err(err).Msg("error getting prefix usage for Subnet") - } + subnetUsage := subnetUsageMap[subnetID] if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > subnetUsage.AvailableIPs { logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID)) return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID), nil) @@ -598,12 +618,8 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { // Check if VPC Prefix is exhausted incomingInterfaceIPs := vpcPrefixIfcMap[vpcPrefixID] - vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) - if err != nil || vpUsage == nil { - // Just log the error and continue - logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") - } - if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > vpUsage.AvailableIPs { + vpUsage := vpcPrefixUsageMap[vpcPrefixID] + if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs)*2 > vpUsage.AvailableIPs { logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID)) return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID), nil) } @@ -2389,6 +2405,30 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Primary VPC ID: %s for Instance must not be listed in `secondaryVpcIds`", vpc.ID), nil) } + subnetsForUsage := make([]*cdbm.Subnet, 0, len(subnetIfcMap)) + for subnetID := range subnetIfcMap { + if sn, ok := subnetIDMap[subnetID]; ok { + subnetsForUsage = append(subnetsForUsage, sn) + } + } + subnetUsageMap, usageErr := sbDAO.GetPrefixUsage(ctx, nil, subnetsForUsage...) + if usageErr != nil { + logger.Error().Err(usageErr).Msg("error getting prefix usage for Subnets") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for Subnet", nil) + } + + vpcPrefixesForUsage := make([]*cdbm.VpcPrefix, 0, len(vpcPrefixIfcMap)) + for vpcPrefixID := range vpcPrefixIfcMap { + if vp, ok := vpcPrefixIDMap[vpcPrefixID]; ok { + vpcPrefixesForUsage = append(vpcPrefixesForUsage, vp) + } + } + vpcPrefixUsageMap, vpUsageErr := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefixesForUsage...) + if vpUsageErr != nil { + logger.Error().Err(vpUsageErr).Msg("error getting prefix usage for VPC Prefixes") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to get prefix usage for VPC Prefix", nil) + } + for _, ifc := range apiRequest.Interfaces { if ifc.SubnetID != nil { subnetID := uuid.MustParse(*ifc.SubnetID) @@ -2418,12 +2458,9 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have Ethernet network virtualization type in order to create Subnet based interfaces", instance.VpcID), nil) } + // Check if Subnet is exhausted incomingInterfaceIPs := subnetIfcMap[subnetID] - existingSubnetIfcMap[subnetID] - subnetUsage, err := sbDAO.GetPrefixUsage(ctx, nil, subnet) - if err != nil { - // Just log the error and continue - logger.Error().Err(err).Msg("error getting prefix usage for Subnet") - } + subnetUsage := subnetUsageMap[subnetID] if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > subnetUsage.AvailableIPs { logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID)) return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID), nil) @@ -2499,13 +2536,9 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("VPC: %v specified in request must have FNN network virtualization type in order to create VPC Prefix based interfaces", instance.VpcID), nil) } + // Check if VPC Prefix is exhausted incomingInterfaceIPs := vpcPrefixIfcMap[vpcPrefixID] - existingVpcPrefixIfcMap[vpcPrefixID] - - vpUsage, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) - if err != nil || vpUsage == nil { - // Just log the error and continue - logger.Error().Err(err).Msg("error getting prefix usage for VPC Prefix") - } + vpUsage := vpcPrefixUsageMap[vpcPrefixID] if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs)*2 > vpUsage.AvailableIPs { logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID)) return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID), nil) diff --git a/rest-api/api/pkg/api/handler/subnet.go b/rest-api/api/pkg/api/handler/subnet.go index b4a3a06f4a..05bf63df71 100644 --- a/rest-api/api/pkg/api/handler/subnet.go +++ b/rest-api/api/pkg/api/handler/subnet.go @@ -548,18 +548,24 @@ func (gash GetAllSubnetHandler) Handle(c echo.Context) error { sbusageMap := map[uuid.UUID]*cip.Usage{} if includeUsageStats { + subnetsForUsage := make([]*cdbm.Subnet, 0, len(subnets)) for i := range subnets { sn := &subnets[i] if sn.IPv4Block == nil { logger.Error().Str("subnetId", sn.ID.String()).Msg("Subnet missing IPv4 Block relation for usage stats") continue } - prefixUsage, serr := sDAO.GetPrefixUsage(ctx, nil, sn) + subnetsForUsage = append(subnetsForUsage, sn) + } + if len(subnetsForUsage) > 0 { + prefixUsageMap, serr := sDAO.GetPrefixUsage(ctx, nil, subnetsForUsage...) if serr != nil { - logger.Error().Err(serr).Str("subnetId", sn.ID.String()).Msg("error retrieving usage stats for Subnet") - continue + logger.Error().Err(serr).Msg("error retrieving usage stats for Subnets") + } else { + for id, usage := range prefixUsageMap { + sbusageMap[id] = usage + } } - sbusageMap[sn.ID] = prefixUsage } } @@ -739,11 +745,17 @@ func (gsh GetSubnetHandler) Handle(c echo.Context) error { logger.Error().Str("subnetId", subnet.ID.String()).Msg("Subnet missing IPv4 Block relation for usage stats") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Usage Stats for Subnet", nil) } - sbusage, err = sDAO.GetPrefixUsage(ctx, nil, subnet) + prefixUsageMap, err := sDAO.GetPrefixUsage(ctx, nil, subnet) if err != nil { logger.Error().Err(err).Msg("error retrieving usage stats for Subnet") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Usage Stats for Subnet", nil) } + var ok bool + sbusage, ok = prefixUsageMap[subnet.ID] + if !ok { + logger.Error().Str("subnetId", subnet.ID.String()).Msg("Subnet missing IPv4 prefix for usage stats") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Usage Stats for Subnet", nil) + } } // Send response diff --git a/rest-api/api/pkg/api/handler/vpcprefix.go b/rest-api/api/pkg/api/handler/vpcprefix.go index 6806ed797e..f6f89df4a5 100644 --- a/rest-api/api/pkg/api/handler/vpcprefix.go +++ b/rest-api/api/pkg/api/handler/vpcprefix.go @@ -495,18 +495,24 @@ func (gash GetAllVpcPrefixHandler) Handle(c echo.Context) error { vpusageMap := map[uuid.UUID]*cip.Usage{} if includeUsageStats { + vpcPrefixesForUsage := make([]*cdbm.VpcPrefix, 0, len(vpcPrefixes)) for i := range vpcPrefixes { vp := &vpcPrefixes[i] if vp.IPBlock == nil { logger.Error().Str("vpcPrefixId", vp.ID.String()).Msg("VPC prefix missing IP Block relation for usage stats") continue } - vpusage, serr := vpcPrefixDAO.GetPrefixUsage(ctx, nil, vp) + vpcPrefixesForUsage = append(vpcPrefixesForUsage, vp) + } + if len(vpcPrefixesForUsage) > 0 { + prefixUsageMap, serr := vpcPrefixDAO.GetPrefixUsage(ctx, nil, vpcPrefixesForUsage...) if serr != nil { - logger.Error().Err(serr).Msg("error retrieving usage stats for VPC prefix") - continue + logger.Error().Err(serr).Msg("error retrieving usage stats for VPC prefixes") + } else { + for id, usage := range prefixUsageMap { + vpusageMap[id] = usage + } } - vpusageMap[vp.ID] = vpusage } } @@ -686,11 +692,17 @@ func (gsh GetVpcPrefixHandler) Handle(c echo.Context) error { logger.Error().Str("vpcPrefixId", vpcPrefix.ID.String()).Msg("VPC prefix missing IP Block relation for usage stats") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Usage Stats for VPC prefix", nil) } - vpusage, err = vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) + prefixUsageMap, err := vpDAO.GetPrefixUsage(ctx, nil, vpcPrefix) if err != nil { logger.Error().Err(err).Msg("error retrieving usage stats for VPC prefix") return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Usage Stats for VPC prefix", nil) } + var ok bool + vpusage, ok = prefixUsageMap[vpcPrefix.ID] + if !ok { + logger.Error().Str("vpcPrefixId", vpcPrefix.ID.String()).Msg("VPC prefix missing CIDR for usage stats") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Usage Stats for VPC prefix", nil) + } } // Send response diff --git a/rest-api/db/pkg/db/model/subnet.go b/rest-api/db/pkg/db/model/subnet.go index 9186ebd9f1..25377ad489 100644 --- a/rest-api/db/pkg/db/model/subnet.go +++ b/rest-api/db/pkg/db/model/subnet.go @@ -405,8 +405,9 @@ type SubnetDAO interface { // Delete(ctx context.Context, tx *db.Tx, id uuid.UUID) error // - // GetPrefixUsage returns IPv4 interface usage for this subnet (in-memory IPAM simulation). - GetPrefixUsage(ctx context.Context, tx *db.Tx, sn *Subnet) (*cipam.Usage, error) + // GetPrefixUsage returns IPv4 interface usage per subnet ID (in-memory IPAM simulation). + // Subnets without an IPv4 prefix are omitted from the result map. + GetPrefixUsage(ctx context.Context, tx *db.Tx, subnets ...*Subnet) (map[uuid.UUID]*cipam.Usage, error) } // SubnetSQLDAO is an implementation of the SubnetDAO interface @@ -855,54 +856,52 @@ func (ssd SubnetSQLDAO) Delete(ctx context.Context, tx *db.Tx, id uuid.UUID) err return nil } -// queryEthernetInterfaceIPsForSubnet returns iface row count and, for interfaces with IPs, -// each interface's assigned addresses. COUNT(*) equals len(rows) for the same join/filter on one SELECT. -func queryEthernetInterfaceIPsForSubnet(ctx context.Context, idb bun.IDB, subnetID uuid.UUID) (ifaceRows int64, ipStrings [][]string, err error) { +func subnetIPv4CIDR(sn *Subnet) (string, bool) { + if sn.IPv4Prefix == nil || *sn.IPv4Prefix == "" { + return "", false + } + if strings.Contains(*sn.IPv4Prefix, "/") { + return *sn.IPv4Prefix, true + } + return fmt.Sprintf("%s/%d", *sn.IPv4Prefix, sn.PrefixLength), true +} + +// queryEthernetInterfaceIPsForSubnets returns per-subnet iface row counts and assigned IP address +// slices for all given subnet IDs in a single query. +func queryEthernetInterfaceIPsForSubnets(ctx context.Context, idb bun.IDB, subnetIDs []uuid.UUID) (ifaceCounts map[uuid.UUID]int64, ifaceIPs map[uuid.UUID][][]string, err error) { + ifaceCounts = make(map[uuid.UUID]int64, len(subnetIDs)) + ifaceIPs = make(map[uuid.UUID][][]string, len(subnetIDs)) + for _, id := range subnetIDs { + ifaceCounts[id] = 0 + ifaceIPs[id] = nil + } + if len(subnetIDs) == 0 { + return ifaceCounts, ifaceIPs, nil + } + type row struct { - IPAddresses []string `bun:"ip_addresses,array"` + SubnetID uuid.UUID `bun:"subnet_id"` + IPAddresses []string `bun:"ip_addresses,array"` } var rows []row err = idb.NewRaw( - `SELECT ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id - WHERE ifc.subnet_id = ? AND ifc.deleted IS NULL AND inst.deleted IS NULL`, - subnetID, + `SELECT ifc.subnet_id, ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id + WHERE ifc.subnet_id IN (?) AND ifc.deleted IS NULL AND inst.deleted IS NULL`, + bun.In(subnetIDs), ).Scan(ctx, &rows) if err != nil { - return 0, nil, err + return nil, nil, err } - count := int64(len(rows)) - ips := make([][]string, 0, len(rows)) for _, r := range rows { + ifaceCounts[r.SubnetID]++ if len(r.IPAddresses) > 0 { - ips = append(ips, r.IPAddresses) + ifaceIPs[r.SubnetID] = append(ifaceIPs[r.SubnetID], r.IPAddresses) } } - return count, ips, nil + return ifaceCounts, ifaceIPs, nil } -// GetPrefixUsage derives IPv4 interface usage stats for this Subnet via an in-memory IPAM simulation. -func (ssd SubnetSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, sn *Subnet) (*cipam.Usage, error) { - if sn == nil { - return nil, fmt.Errorf("Failed to calculate usage stats for Subnet: nil argument specified") - } - - if sn.IPv4Prefix == nil || *sn.IPv4Prefix == "" { - return nil, fmt.Errorf("Failed to calculate usage stats for Subnet %q: %w", sn.ID.String(), errSubnetNoIPv4Prefix) - } - - var cidr string - if strings.Contains(*sn.IPv4Prefix, "/") { - cidr = *sn.IPv4Prefix - } else { - cidr = fmt.Sprintf("%s/%d", *sn.IPv4Prefix, sn.PrefixLength) - } - - idb := db.GetIDB(tx, ssd.dbSession) - ifcCount, ips, err := queryEthernetInterfaceIPsForSubnet(ctx, idb, sn.ID) - if err != nil { - return nil, err - } - +func subnetPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int64, ips [][]string) (*cipam.Usage, error) { ipamer := cipam.New(ctx) ipamPrefix, err := ipamer.NewPrefix(ctx, cidr) if err != nil { @@ -951,6 +950,46 @@ func (ssd SubnetSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, sn *Subne }, nil } +// GetPrefixUsage derives IPv4 interface usage stats for each Subnet via in-memory IPAM simulation. +func (ssd SubnetSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, subnets ...*Subnet) (map[uuid.UUID]*cipam.Usage, error) { + if len(subnets) == 0 { + return map[uuid.UUID]*cipam.Usage{}, nil + } + + subnetCIDRs := make(map[uuid.UUID]string, len(subnets)) + subnetIDs := make([]uuid.UUID, 0, len(subnets)) + for _, sn := range subnets { + if sn == nil { + return nil, fmt.Errorf("Failed to calculate usage stats for Subnet: nil argument specified") + } + cidr, ok := subnetIPv4CIDR(sn) + if !ok { + continue + } + subnetCIDRs[sn.ID] = cidr + subnetIDs = append(subnetIDs, sn.ID) + } + if len(subnetIDs) == 0 { + return map[uuid.UUID]*cipam.Usage{}, nil + } + + idb := db.GetIDB(tx, ssd.dbSession) + ifcCounts, ifcIPs, err := queryEthernetInterfaceIPsForSubnets(ctx, idb, subnetIDs) + if err != nil { + return nil, err + } + + usageByID := make(map[uuid.UUID]*cipam.Usage, len(subnetIDs)) + for _, subnetID := range subnetIDs { + usage, uerr := subnetPrefixUsageFromInterfaces(ctx, subnetCIDRs[subnetID], ifcCounts[subnetID], ifcIPs[subnetID]) + if uerr != nil { + return nil, uerr + } + usageByID[subnetID] = usage + } + return usageByID, nil +} + // NewSubnetDAO returns a new SubnetDAO func NewSubnetDAO(dbSession *db.Session) SubnetDAO { return &SubnetSQLDAO{ diff --git a/rest-api/db/pkg/db/model/vpcprefix.go b/rest-api/db/pkg/db/model/vpcprefix.go index b78a964cb5..c9a0042713 100644 --- a/rest-api/db/pkg/db/model/vpcprefix.go +++ b/rest-api/db/pkg/db/model/vpcprefix.go @@ -250,8 +250,9 @@ type VpcPrefixDAO interface { // Delete(ctx context.Context, tx *db.Tx, id uuid.UUID) error // - // GetPrefixUsage returns IPv4 interface usage for this VPC prefix (in-memory IPAM simulation). - GetPrefixUsage(ctx context.Context, tx *db.Tx, vp *VpcPrefix) (*cipam.Usage, error) + // GetPrefixUsage returns IPv4 interface usage per VPC prefix ID (in-memory IPAM simulation). + // VPC prefixes without a valid CIDR are omitted from the result map. + GetPrefixUsage(ctx context.Context, tx *db.Tx, vpcPrefixes ...*VpcPrefix) (map[uuid.UUID]*cipam.Usage, error) } // VpcPrefixSQLDAO is an implementation of the VpcPrefixDAO interface @@ -524,57 +525,56 @@ func (vpsd VpcPrefixSQLDAO) Delete(ctx context.Context, tx *db.Tx, id uuid.UUID) return nil } -// queryEthernetInterfaceIPsForVPCPrefix returns iface row count (all matching ethernet interfaces) -// and, for each row with assigned IPs, a slice of that interface's IPv4 addresses. -// One SELECT suffices: COUNT(*) equals the number of result rows given the same join/filter. -func queryEthernetInterfaceIPsForVPCPrefix(ctx context.Context, idb bun.IDB, vpcPrefixID uuid.UUID) (ifaceRows int64, ipStrings [][]string, err error) { +func vpcPrefixCIDR(vp *VpcPrefix) (string, bool) { + var cidr string + if strings.Contains(vp.Prefix, "/") { + cidr = vp.Prefix + } else { + cidr = fmt.Sprintf("%s/%d", vp.Prefix, vp.PrefixLength) + } + if cidr == "" { + return "", false + } + return cidr, true +} + +// queryEthernetInterfaceIPsForVPCPrefixes returns per-VPC-prefix iface row counts and assigned IP +// address slices for all given VPC prefix IDs in a single query. +func queryEthernetInterfaceIPsForVPCPrefixes(ctx context.Context, idb bun.IDB, vpcPrefixIDs []uuid.UUID) (ifaceCounts map[uuid.UUID]int64, ifaceIPs map[uuid.UUID][][]string, err error) { + ifaceCounts = make(map[uuid.UUID]int64, len(vpcPrefixIDs)) + ifaceIPs = make(map[uuid.UUID][][]string, len(vpcPrefixIDs)) + for _, id := range vpcPrefixIDs { + ifaceCounts[id] = 0 + ifaceIPs[id] = nil + } + if len(vpcPrefixIDs) == 0 { + return ifaceCounts, ifaceIPs, nil + } + type row struct { - IPAddresses []string `bun:"ip_addresses,array"` + VpcPrefixID uuid.UUID `bun:"vpc_prefix_id"` + IPAddresses []string `bun:"ip_addresses,array"` } var rows []row err = idb.NewRaw( - `SELECT ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id - WHERE ifc.vpc_prefix_id = ? AND ifc.deleted IS NULL AND inst.deleted IS NULL + `SELECT ifc.vpc_prefix_id, ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id + WHERE ifc.vpc_prefix_id IN (?) AND ifc.deleted IS NULL AND inst.deleted IS NULL AND inst.status NOT IN ('Terminating', 'Terminated')`, - vpcPrefixID, + bun.In(vpcPrefixIDs), ).Scan(ctx, &rows) if err != nil { - return 0, nil, err + return nil, nil, err } - count := int64(len(rows)) - ips := make([][]string, 0, len(rows)) for _, r := range rows { + ifaceCounts[r.VpcPrefixID]++ if len(r.IPAddresses) > 0 { - ips = append(ips, r.IPAddresses) + ifaceIPs[r.VpcPrefixID] = append(ifaceIPs[r.VpcPrefixID], r.IPAddresses) } } - return count, ips, nil + return ifaceCounts, ifaceIPs, nil } -// GetPrefixUsage derives IPv4 interface usage stats for this VpcPrefix via an in-memory IPAM simulation. -func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vp *VpcPrefix) (*cipam.Usage, error) { - if vp == nil { - return nil, fmt.Errorf("Failed to calculate usage stats for VPC Prefix: nil argument") - } - - var cidr string - if strings.Contains(vp.Prefix, "/") { - cidr = vp.Prefix - } else { - cidr = fmt.Sprintf("%s/%d", vp.Prefix, vp.PrefixLength) - } - if cidr == "" { - return nil, fmt.Errorf("Failed to calculate usage stats for VPC Prefix %q: CIDR could not be populated", vp.ID.String()) - } - - // Query the IP addresses for each Interface associated with this VPC Prefix - idb := db.GetIDB(tx, vpsd.dbSession) - ifcCount, ips, err := queryEthernetInterfaceIPsForVPCPrefix(ctx, idb, vp.ID) - if err != nil { - return nil, err - } - - // derive the usage stats via an in-memory IPAM simulation +func vpcPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int64, ips [][]string) (*cipam.Usage, error) { ipamer := cipam.New(ctx) ipamPrefix, err := ipamer.NewPrefix(ctx, cidr) if err != nil { @@ -587,8 +587,6 @@ func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vp *V return nil, err } - // track acquired prefixes to avoid duplicates - // each interface IP address consumes 2 /31 prefixes acquiredPrefixes := make(map[string]struct{}) for _, ipAddresses := range ips { for _, ipStr := range ipAddresses { @@ -599,7 +597,6 @@ func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vp *V if !netIpPrefix.Contains(netIpAddr) { continue } - // derive the /31 prefix for the IP address contained31Prefix, perr := netIpAddr.Prefix(31) if perr != nil { continue @@ -636,6 +633,46 @@ func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vp *V }, nil } +// GetPrefixUsage derives IPv4 interface usage stats for each VpcPrefix via in-memory IPAM simulation. +func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vpcPrefixes ...*VpcPrefix) (map[uuid.UUID]*cipam.Usage, error) { + if len(vpcPrefixes) == 0 { + return map[uuid.UUID]*cipam.Usage{}, nil + } + + vpcPrefixCIDRs := make(map[uuid.UUID]string, len(vpcPrefixes)) + vpcPrefixIDs := make([]uuid.UUID, 0, len(vpcPrefixes)) + for _, vp := range vpcPrefixes { + if vp == nil { + return nil, fmt.Errorf("Failed to calculate usage stats for VPC Prefix: nil argument") + } + cidr, ok := vpcPrefixCIDR(vp) + if !ok { + continue + } + vpcPrefixCIDRs[vp.ID] = cidr + vpcPrefixIDs = append(vpcPrefixIDs, vp.ID) + } + if len(vpcPrefixIDs) == 0 { + return map[uuid.UUID]*cipam.Usage{}, nil + } + + idb := db.GetIDB(tx, vpsd.dbSession) + ifcCounts, ifcIPs, err := queryEthernetInterfaceIPsForVPCPrefixes(ctx, idb, vpcPrefixIDs) + if err != nil { + return nil, err + } + + usageByID := make(map[uuid.UUID]*cipam.Usage, len(vpcPrefixIDs)) + for _, vpcPrefixID := range vpcPrefixIDs { + usage, uerr := vpcPrefixUsageFromInterfaces(ctx, vpcPrefixCIDRs[vpcPrefixID], ifcCounts[vpcPrefixID], ifcIPs[vpcPrefixID]) + if uerr != nil { + return nil, uerr + } + usageByID[vpcPrefixID] = usage + } + return usageByID, nil +} + // NewVpcPrefixDAO returns a new VpcPrefixDAO func NewVpcPrefixDAO(dbSession *db.Session) VpcPrefixDAO { return &VpcPrefixSQLDAO{ From baa0a2baad7ad714e218e44163bbae3c731fa940 Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Wed, 10 Jun 2026 17:38:50 -0700 Subject: [PATCH 5/7] Fix the suggested by review --- rest-api/db/pkg/db/model/subnet.go | 106 +++++++++----------- rest-api/db/pkg/db/model/vpcprefix.go | 135 ++++++++++++-------------- 2 files changed, 107 insertions(+), 134 deletions(-) diff --git a/rest-api/db/pkg/db/model/subnet.go b/rest-api/db/pkg/db/model/subnet.go index 25377ad489..5c85745f0e 100644 --- a/rest-api/db/pkg/db/model/subnet.go +++ b/rest-api/db/pkg/db/model/subnet.go @@ -856,52 +856,19 @@ func (ssd SubnetSQLDAO) Delete(ctx context.Context, tx *db.Tx, id uuid.UUID) err return nil } -func subnetIPv4CIDR(sn *Subnet) (string, bool) { - if sn.IPv4Prefix == nil || *sn.IPv4Prefix == "" { - return "", false +// GetIPv4CIDR returns the subnet's IPv4 CIDR string, or nil when IPv4Prefix is unset. +func (s *Subnet) GetIPv4CIDR() *string { + if s.IPv4Prefix == nil || *s.IPv4Prefix == "" { + return nil } - if strings.Contains(*sn.IPv4Prefix, "/") { - return *sn.IPv4Prefix, true + if strings.Contains(*s.IPv4Prefix, "/") { + return s.IPv4Prefix } - return fmt.Sprintf("%s/%d", *sn.IPv4Prefix, sn.PrefixLength), true + cidr := fmt.Sprintf("%s/%d", *s.IPv4Prefix, s.PrefixLength) + return &cidr } -// queryEthernetInterfaceIPsForSubnets returns per-subnet iface row counts and assigned IP address -// slices for all given subnet IDs in a single query. -func queryEthernetInterfaceIPsForSubnets(ctx context.Context, idb bun.IDB, subnetIDs []uuid.UUID) (ifaceCounts map[uuid.UUID]int64, ifaceIPs map[uuid.UUID][][]string, err error) { - ifaceCounts = make(map[uuid.UUID]int64, len(subnetIDs)) - ifaceIPs = make(map[uuid.UUID][][]string, len(subnetIDs)) - for _, id := range subnetIDs { - ifaceCounts[id] = 0 - ifaceIPs[id] = nil - } - if len(subnetIDs) == 0 { - return ifaceCounts, ifaceIPs, nil - } - - type row struct { - SubnetID uuid.UUID `bun:"subnet_id"` - IPAddresses []string `bun:"ip_addresses,array"` - } - var rows []row - err = idb.NewRaw( - `SELECT ifc.subnet_id, ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id - WHERE ifc.subnet_id IN (?) AND ifc.deleted IS NULL AND inst.deleted IS NULL`, - bun.In(subnetIDs), - ).Scan(ctx, &rows) - if err != nil { - return nil, nil, err - } - for _, r := range rows { - ifaceCounts[r.SubnetID]++ - if len(r.IPAddresses) > 0 { - ifaceIPs[r.SubnetID] = append(ifaceIPs[r.SubnetID], r.IPAddresses) - } - } - return ifaceCounts, ifaceIPs, nil -} - -func subnetPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int64, ips [][]string) (*cipam.Usage, error) { +func subnetPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int64, ips []string) (*cipam.Usage, error) { ipamer := cipam.New(ctx) ipamPrefix, err := ipamer.NewPrefix(ctx, cidr) if err != nil { @@ -914,19 +881,17 @@ func subnetPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount return nil, err } - for _, ipAddresses := range ips { - for _, ipStr := range ipAddresses { - netIpAddr, ierr := netip.ParseAddr(strings.TrimSpace(ipStr)) - if ierr != nil || !netIpAddr.Is4() { - continue - } - if !netIpPrefix.Contains(netIpAddr) { - continue - } - _, ierr = ipamer.AcquireSpecificIP(ctx, validatedCidr, netIpAddr.String()) - if ierr != nil { - continue - } + for _, ipStr := range ips { + netIpAddr, ierr := netip.ParseAddr(strings.TrimSpace(ipStr)) + if ierr != nil || !netIpAddr.Is4() { + continue + } + if !netIpPrefix.Contains(netIpAddr) { + continue + } + _, ierr = ipamer.AcquireSpecificIP(ctx, validatedCidr, netIpAddr.String()) + if ierr != nil { + continue } } @@ -962,11 +927,11 @@ func (ssd SubnetSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, subnets . if sn == nil { return nil, fmt.Errorf("Failed to calculate usage stats for Subnet: nil argument specified") } - cidr, ok := subnetIPv4CIDR(sn) - if !ok { + cidr := sn.GetIPv4CIDR() + if cidr == nil { continue } - subnetCIDRs[sn.ID] = cidr + subnetCIDRs[sn.ID] = *cidr subnetIDs = append(subnetIDs, sn.ID) } if len(subnetIDs) == 0 { @@ -974,10 +939,33 @@ func (ssd SubnetSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, subnets . } idb := db.GetIDB(tx, ssd.dbSession) - ifcCounts, ifcIPs, err := queryEthernetInterfaceIPsForSubnets(ctx, idb, subnetIDs) + + ifcCounts := make(map[uuid.UUID]int64, len(subnetIDs)) + ifcIPs := make(map[uuid.UUID][]string, len(subnetIDs)) + for _, id := range subnetIDs { + ifcCounts[id] = 0 + ifcIPs[id] = nil + } + + type row struct { + SubnetID uuid.UUID `bun:"subnet_id"` + IPAddresses []string `bun:"ip_addresses,array"` + } + var rows []row + err := idb.NewRaw( + `SELECT ifc.subnet_id, ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id + WHERE ifc.subnet_id IN (?) AND ifc.deleted IS NULL AND inst.deleted IS NULL`, + bun.In(subnetIDs), + ).Scan(ctx, &rows) if err != nil { return nil, err } + for _, r := range rows { + ifcCounts[r.SubnetID]++ + if len(r.IPAddresses) > 0 { + ifcIPs[r.SubnetID] = append(ifcIPs[r.SubnetID], r.IPAddresses...) + } + } usageByID := make(map[uuid.UUID]*cipam.Usage, len(subnetIDs)) for _, subnetID := range subnetIDs { diff --git a/rest-api/db/pkg/db/model/vpcprefix.go b/rest-api/db/pkg/db/model/vpcprefix.go index c9a0042713..45b1f32259 100644 --- a/rest-api/db/pkg/db/model/vpcprefix.go +++ b/rest-api/db/pkg/db/model/vpcprefix.go @@ -110,6 +110,18 @@ func (vp *VpcPrefix) ToProto(vpc *Vpc) *cwssaws.VpcPrefix { return proto } +// GetIPv4CIDR returns the VPC prefix's IPv4 CIDR string, or nil when Prefix is unset. +func (vp *VpcPrefix) GetIPv4CIDR() *string { + if vp.Prefix == "" { + return nil + } + if strings.Contains(vp.Prefix, "/") { + return &vp.Prefix + } + cidr := fmt.Sprintf("%s/%d", vp.Prefix, vp.PrefixLength) + return &cidr +} + // FromProto populates this VpcPrefix from its workflow proto representation. // A nil proto is a no-op. This is the inverse of `ToProto` and exists for // convention symmetry — currently no code path on the cloud side @@ -525,56 +537,7 @@ func (vpsd VpcPrefixSQLDAO) Delete(ctx context.Context, tx *db.Tx, id uuid.UUID) return nil } -func vpcPrefixCIDR(vp *VpcPrefix) (string, bool) { - var cidr string - if strings.Contains(vp.Prefix, "/") { - cidr = vp.Prefix - } else { - cidr = fmt.Sprintf("%s/%d", vp.Prefix, vp.PrefixLength) - } - if cidr == "" { - return "", false - } - return cidr, true -} - -// queryEthernetInterfaceIPsForVPCPrefixes returns per-VPC-prefix iface row counts and assigned IP -// address slices for all given VPC prefix IDs in a single query. -func queryEthernetInterfaceIPsForVPCPrefixes(ctx context.Context, idb bun.IDB, vpcPrefixIDs []uuid.UUID) (ifaceCounts map[uuid.UUID]int64, ifaceIPs map[uuid.UUID][][]string, err error) { - ifaceCounts = make(map[uuid.UUID]int64, len(vpcPrefixIDs)) - ifaceIPs = make(map[uuid.UUID][][]string, len(vpcPrefixIDs)) - for _, id := range vpcPrefixIDs { - ifaceCounts[id] = 0 - ifaceIPs[id] = nil - } - if len(vpcPrefixIDs) == 0 { - return ifaceCounts, ifaceIPs, nil - } - - type row struct { - VpcPrefixID uuid.UUID `bun:"vpc_prefix_id"` - IPAddresses []string `bun:"ip_addresses,array"` - } - var rows []row - err = idb.NewRaw( - `SELECT ifc.vpc_prefix_id, ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id - WHERE ifc.vpc_prefix_id IN (?) AND ifc.deleted IS NULL AND inst.deleted IS NULL - AND inst.status NOT IN ('Terminating', 'Terminated')`, - bun.In(vpcPrefixIDs), - ).Scan(ctx, &rows) - if err != nil { - return nil, nil, err - } - for _, r := range rows { - ifaceCounts[r.VpcPrefixID]++ - if len(r.IPAddresses) > 0 { - ifaceIPs[r.VpcPrefixID] = append(ifaceIPs[r.VpcPrefixID], r.IPAddresses) - } - } - return ifaceCounts, ifaceIPs, nil -} - -func vpcPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int64, ips [][]string) (*cipam.Usage, error) { +func vpcPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int64, ips []string) (*cipam.Usage, error) { ipamer := cipam.New(ctx) ipamPrefix, err := ipamer.NewPrefix(ctx, cidr) if err != nil { @@ -588,28 +551,26 @@ func vpcPrefixUsageFromInterfaces(ctx context.Context, cidr string, ifcCount int } acquiredPrefixes := make(map[string]struct{}) - for _, ipAddresses := range ips { - for _, ipStr := range ipAddresses { - netIpAddr, ierr := netip.ParseAddr(strings.TrimSpace(ipStr)) - if ierr != nil || !netIpAddr.Is4() { - continue - } - if !netIpPrefix.Contains(netIpAddr) { - continue - } - contained31Prefix, perr := netIpAddr.Prefix(31) - if perr != nil { - continue - } - k := contained31Prefix.Masked().String() - if _, dup := acquiredPrefixes[k]; dup { - continue - } - if _, ierr := ipamer.AcquireSpecificChildPrefix(ctx, validatedCidr, k); ierr != nil { - continue - } - acquiredPrefixes[k] = struct{}{} + for _, ipStr := range ips { + netIpAddr, ierr := netip.ParseAddr(strings.TrimSpace(ipStr)) + if ierr != nil || !netIpAddr.Is4() { + continue + } + if !netIpPrefix.Contains(netIpAddr) { + continue + } + contained31Prefix, perr := netIpAddr.Prefix(31) + if perr != nil { + continue } + k := contained31Prefix.Masked().String() + if _, dup := acquiredPrefixes[k]; dup { + continue + } + if _, ierr := ipamer.AcquireSpecificChildPrefix(ctx, validatedCidr, k); ierr != nil { + continue + } + acquiredPrefixes[k] = struct{}{} } ipamPrefix = ipamer.PrefixFrom(ctx, validatedCidr) @@ -645,11 +606,11 @@ func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vpcPr if vp == nil { return nil, fmt.Errorf("Failed to calculate usage stats for VPC Prefix: nil argument") } - cidr, ok := vpcPrefixCIDR(vp) - if !ok { + cidr := vp.GetIPv4CIDR() + if cidr == nil { continue } - vpcPrefixCIDRs[vp.ID] = cidr + vpcPrefixCIDRs[vp.ID] = *cidr vpcPrefixIDs = append(vpcPrefixIDs, vp.ID) } if len(vpcPrefixIDs) == 0 { @@ -657,10 +618,34 @@ func (vpsd VpcPrefixSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, vpcPr } idb := db.GetIDB(tx, vpsd.dbSession) - ifcCounts, ifcIPs, err := queryEthernetInterfaceIPsForVPCPrefixes(ctx, idb, vpcPrefixIDs) + + ifcCounts := make(map[uuid.UUID]int64, len(vpcPrefixIDs)) + ifcIPs := make(map[uuid.UUID][]string, len(vpcPrefixIDs)) + for _, id := range vpcPrefixIDs { + ifcCounts[id] = 0 + ifcIPs[id] = nil + } + + type row struct { + VpcPrefixID uuid.UUID `bun:"vpc_prefix_id"` + IPAddresses []string `bun:"ip_addresses,array"` + } + var rows []row + err := idb.NewRaw( + `SELECT ifc.vpc_prefix_id, ifc.ip_addresses FROM "interface" AS ifc INNER JOIN instance AS inst ON inst.id = ifc.instance_id + WHERE ifc.vpc_prefix_id IN (?) AND ifc.deleted IS NULL AND inst.deleted IS NULL + AND inst.status NOT IN ('Terminating', 'Terminated')`, + bun.In(vpcPrefixIDs), + ).Scan(ctx, &rows) if err != nil { return nil, err } + for _, r := range rows { + ifcCounts[r.VpcPrefixID]++ + if len(r.IPAddresses) > 0 { + ifcIPs[r.VpcPrefixID] = append(ifcIPs[r.VpcPrefixID], r.IPAddresses...) + } + } usageByID := make(map[uuid.UUID]*cipam.Usage, len(vpcPrefixIDs)) for _, vpcPrefixID := range vpcPrefixIDs { From aafb07d8a9bd21f80f2284da7f3dac501675da2d Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 11 Jun 2026 12:55:06 -0700 Subject: [PATCH 6/7] Improvised error messages for Usage --- rest-api/api/pkg/api/handler/instance.go | 32 ++++++++++++++----- rest-api/api/pkg/api/handler/instance_test.go | 19 +++++++++-- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index 1dfc3bb65f..c383632448 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -541,8 +541,12 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { incomingInterfaceIPs := subnetIfcMap[subnetID] subnetUsage := subnetUsageMap[subnetID] if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > subnetUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID), nil) + msg := fmt.Sprintf( + "Subnet %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d interface(s) in this request require %d IP address(es)", + subnetID, subnetUsage.AvailableIPs-subnetUsage.AcquiredIPs, subnetUsage.AvailableIPs, incomingInterfaceIPs, incomingInterfaceIPs, + ) + logger.Warn().Msg(msg) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, msg, nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ @@ -620,8 +624,12 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { incomingInterfaceIPs := vpcPrefixIfcMap[vpcPrefixID] vpUsage := vpcPrefixUsageMap[vpcPrefixID] if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs)*2 > vpUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID), nil) + msg := fmt.Sprintf( + "VPC Prefix %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d interface(s) in this request require %d IP addresses", + vpcPrefixID, vpUsage.AvailableIPs-vpUsage.AcquiredIPs, vpUsage.AvailableIPs, incomingInterfaceIPs, incomingInterfaceIPs*2, + ) + logger.Warn().Msg(msg) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, msg, nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ @@ -2462,8 +2470,12 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { incomingInterfaceIPs := subnetIfcMap[subnetID] - existingSubnetIfcMap[subnetID] subnetUsage := subnetUsageMap[subnetID] if subnetUsage != nil && subnetUsage.AvailableIPs > 0 && subnetUsage.AcquiredIPs+uint64(incomingInterfaceIPs) > subnetUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetID), nil) + msg := fmt.Sprintf( + "Subnet %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d additional interface(s) in this request require %d IP address(es)", + subnetID, subnetUsage.AvailableIPs-subnetUsage.AcquiredIPs, subnetUsage.AvailableIPs, incomingInterfaceIPs, incomingInterfaceIPs, + ) + logger.Warn().Msg(msg) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, msg, nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ @@ -2540,8 +2552,12 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { incomingInterfaceIPs := vpcPrefixIfcMap[vpcPrefixID] - existingVpcPrefixIfcMap[vpcPrefixID] vpUsage := vpcPrefixUsageMap[vpcPrefixID] if vpUsage != nil && vpUsage.AvailableIPs > 0 && vpUsage.AcquiredIPs+uint64(incomingInterfaceIPs)*2 > vpUsage.AvailableIPs { - logger.Warn().Msg(fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID)) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixID), nil) + msg := fmt.Sprintf( + "VPC Prefix %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d additional interface(s) in this request require %d IP addresses", + vpcPrefixID, vpUsage.AvailableIPs-vpUsage.AcquiredIPs, vpUsage.AvailableIPs, incomingInterfaceIPs, incomingInterfaceIPs*2, + ) + logger.Warn().Msg(msg) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, msg, nil) } dbInterfaces = append(dbInterfaces, cdbm.Interface{ diff --git a/rest-api/api/pkg/api/handler/instance_test.go b/rest-api/api/pkg/api/handler/instance_test.go index 3de51ada22..d463ad4a8d 100644 --- a/rest-api/api/pkg/api/handler/instance_test.go +++ b/rest-api/api/pkg/api/handler/instance_test.go @@ -1185,6 +1185,15 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { exhaustInst := testInstanceBuildInstance(t, dbSession, fmt.Sprintf("exhaust-vpcprefix-inst-%d", i), tn1.ID, ip.ID, st1.ID, &istExhaustFixture.ID, vpc9.ID, nil, &os1.ID, nil, cdbm.InstanceStatusReady) testInstanceBuildInstanceInterface(t, dbSession, exhaustInst.ID, nil, &vpcPrefixExhausted.ID, nil, cdbm.InterfaceStatusPending) } + + subnetExhaustedUsageMap, err := cdbm.NewSubnetDAO(dbSession).GetPrefixUsage(context.Background(), nil, subnetExhausted) + assert.Nil(t, err) + subnetExhaustedUsage := subnetExhaustedUsageMap[subnetExhausted.ID] + + vpcPrefixExhaustedUsageMap, err := cdbm.NewVpcPrefixDAO(dbSession).GetPrefixUsage(context.Background(), nil, vpcPrefixExhausted) + assert.Nil(t, err) + vpcPrefixExhaustedUsage := vpcPrefixExhaustedUsageMap[vpcPrefixExhausted.ID] + // NvLink Logical Partition nvllp1 := testBuildNVLinkLogicalPartition(t, dbSession, "test-nvllp-1", cutil.GetPtr("Test NVLink Logical Partition"), tnOrg, st1, tn1, cutil.GetPtr(cdbm.NVLinkLogicalPartitionStatusReady), false) assert.NotNil(t, nvllp1) @@ -3486,7 +3495,10 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { reqOrg: tnOrg, reqUser: tnu1, respCode: http.StatusBadRequest, - respMessage: fmt.Sprintf("Ip Addresses for Subnet ID: %v specified in interfaces data in request are exhausted", subnetExhausted.ID), + respMessage: fmt.Sprintf( + "Subnet %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d interface(s) in this request require %d IP address(es)", + subnetExhausted.ID, subnetExhaustedUsage.AvailableIPs-subnetExhaustedUsage.AcquiredIPs, subnetExhaustedUsage.AvailableIPs, 1, 1, + ), }, wantErr: false, }, @@ -3516,7 +3528,10 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { reqOrg: tnOrg, reqUser: tnu1, respCode: http.StatusBadRequest, - respMessage: fmt.Sprintf("Ip Addresses for VPC Prefix ID: %v specified in interfaces data in request are exhausted", vpcPrefixExhausted.ID), + respMessage: fmt.Sprintf( + "VPC Prefix %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d interface(s) in this request require %d IP addresses", + vpcPrefixExhausted.ID, vpcPrefixExhaustedUsage.AvailableIPs-vpcPrefixExhaustedUsage.AcquiredIPs, vpcPrefixExhaustedUsage.AvailableIPs, 1, 2, + ), }, wantErr: false, }, From 7478b2c712f86ce713ad13bbe3f3a583440f5f12 Mon Sep 17 00:00:00 2001 From: Hitesh Wadekar Date: Thu, 11 Jun 2026 13:30:50 -0700 Subject: [PATCH 7/7] Fix go fmt --- rest-api/api/pkg/api/handler/instance_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest-api/api/pkg/api/handler/instance_test.go b/rest-api/api/pkg/api/handler/instance_test.go index d463ad4a8d..9851454d89 100644 --- a/rest-api/api/pkg/api/handler/instance_test.go +++ b/rest-api/api/pkg/api/handler/instance_test.go @@ -3492,9 +3492,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { }, }, }, - reqOrg: tnOrg, - reqUser: tnu1, - respCode: http.StatusBadRequest, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusBadRequest, respMessage: fmt.Sprintf( "Subnet %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d interface(s) in this request require %d IP address(es)", subnetExhausted.ID, subnetExhaustedUsage.AvailableIPs-subnetExhaustedUsage.AcquiredIPs, subnetExhaustedUsage.AvailableIPs, 1, 1, @@ -3525,9 +3525,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { }, }, }, - reqOrg: tnOrg, - reqUser: tnu1, - respCode: http.StatusBadRequest, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusBadRequest, respMessage: fmt.Sprintf( "VPC Prefix %v does not have enough IP addresses: %d of %d IP addresses remain available, but the %d interface(s) in this request require %d IP addresses", vpcPrefixExhausted.ID, vpcPrefixExhaustedUsage.AvailableIPs-vpcPrefixExhaustedUsage.AcquiredIPs, vpcPrefixExhaustedUsage.AvailableIPs, 1, 2,