diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index 16852d0876..1dfc3bb65f 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]++ } } @@ -479,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) @@ -509,6 +537,14 @@ 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 is exhausted + 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) + } + dbInterfaces = append(dbInterfaces, cdbm.Interface{ SubnetID: &subnetID, IsPhysical: ifc.IsPhysical, @@ -580,6 +616,14 @@ 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 + 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) + } + 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. @@ -2262,6 +2306,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 { @@ -2271,6 +2317,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) @@ -2279,6 +2326,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]++ } } @@ -2308,6 +2356,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 @@ -2337,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) @@ -2366,6 +2458,14 @@ 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 := 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) + } + dbInterfaces = append(dbInterfaces, cdbm.Interface{ SubnetID: &subnetID, IsPhysical: ifc.IsPhysical, @@ -2436,6 +2536,14 @@ 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 := 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) + } + 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 2671f354dc..3de51ada22 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,12 @@ 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) + 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) @@ -884,6 +892,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 +1177,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 +3462,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) { 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..5c85745f0e 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,19 @@ 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) { - type row struct { - IPAddresses []string `bun:"ip_addresses,array"` +// 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 } - 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, - ).Scan(ctx, &rows) - if err != nil { - return 0, nil, err + if strings.Contains(*s.IPv4Prefix, "/") { + return s.IPv4Prefix } - count := int64(len(rows)) - ips := make([][]string, 0, len(rows)) - for _, r := range rows { - if len(r.IPAddresses) > 0 { - ips = append(ips, r.IPAddresses) - } - } - return count, ips, nil + cidr := fmt.Sprintf("%s/%d", *s.IPv4Prefix, s.PrefixLength) + return &cidr } -// 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 { @@ -915,19 +881,17 @@ func (ssd SubnetSQLDAO) GetPrefixUsage(ctx context.Context, tx *db.Tx, sn *Subne 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 } } @@ -951,6 +915,69 @@ 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 := sn.GetIPv4CIDR() + if cidr == nil { + 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 := 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 { + 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..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 @@ -250,8 +262,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 +537,7 @@ 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) { - type row struct { - 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 - AND inst.status NOT IN ('Terminating', 'Terminated')`, - vpcPrefixID, - ).Scan(ctx, &rows) - if err != nil { - return 0, nil, err - } - count := int64(len(rows)) - ips := make([][]string, 0, len(rows)) - for _, r := range rows { - if len(r.IPAddresses) > 0 { - ips = append(ips, r.IPAddresses) - } - } - return count, ips, 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,32 +550,27 @@ 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 { - netIpAddr, ierr := netip.ParseAddr(strings.TrimSpace(ipStr)) - if ierr != nil || !netIpAddr.Is4() { - continue - } - if !netIpPrefix.Contains(netIpAddr) { - continue - } - // derive the /31 prefix for the IP address - 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) @@ -636,6 +594,70 @@ 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 := vp.GetIPv4CIDR() + if cidr == nil { + 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 := 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 { + 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{