From 707826359b29f6ce610bbde57f3ae94261d407f6 Mon Sep 17 00:00:00 2001 From: ionutboangiu Date: Thu, 7 Aug 2025 12:26:38 +0300 Subject: [PATCH] ips: add ClearAllocations API --- apis/ips.go | 6 +++++ docs/ips.rst | 58 +++++++++++++++++++++++++++++++++++++++------- ips/apis.go | 34 ++++++++++++++++++++++++++- ips/ips_it_test.go | 56 ++++++++++++++++++++++++-------------------- utils/consts.go | 1 + utils/ips.go | 49 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 35 deletions(-) diff --git a/apis/ips.go b/apis/ips.go index 1c5f6f2cd..c39f25bf4 100644 --- a/apis/ips.go +++ b/apis/ips.go @@ -211,3 +211,9 @@ func (ipS *IPSv1) V1ReleaseIP(ctx *context.Context, args *utils.CGREvent, reply func (ipS *IPSv1) V1GetIPAllocations(ctx *context.Context, arg *utils.TenantIDWithAPIOpts, reply *utils.IPAllocations) error { return ipS.ips.V1GetIPAllocations(ctx, arg, reply) } + +// V1ClearIPAllocations clears IP allocations from an IPAllocations object. +// If args.AllocationIDs is empty or nil, all allocations will be cleared. +func (ipS *IPSv1) V1ClearIPAllocations(ctx *context.Context, arg *utils.ClearIPAllocationsArgs, reply *string) error { + return ipS.ips.V1ClearIPAllocations(ctx, arg, reply) +} diff --git a/docs/ips.rst b/docs/ips.rst index 107940fab..a1b663871 100644 --- a/docs/ips.rst +++ b/docs/ips.rst @@ -154,15 +154,21 @@ Checks if an IP can be allocated without actually allocating it (dry run). :: { - "Tenant": "cgrates.org", - "ID": "unique_event_id", - "Event": { - "Account": "1001", - "Destination": "1002" - }, - "APIOpts": { - "*ipAllocationID": "ip_allocation_abc123" - } + "method": "IPsV1.AuthorizeIP", + "params": [ + { + "Tenant": "cgrates.org", + "ID": "unique_event_id", + "Event": { + "Account": "1001", + "Destination": "1002" + }, + "APIOpts": { + "*ipAllocationID": "ip_allocation_abc123" + } + } + ], + "id": 1 } **Returns:** @@ -236,6 +242,40 @@ Gets the matching IPAllocations object for a specific event. - IPAllocations object for the matching profile +V1ClearIPAllocations +~~~~~~~~~~~~~~~~~~~~ + +Clears IP allocations from an IPAllocations object. + +**Request:** + +:: + + { + "method": "IPsV1.ClearIPAllocations", + "params": [ + { + "Tenant": "cgrates.org", + "ID": "profile_id", + "AllocationIDs": [ + "alloc1", + "alloc2" + ] + } + ], + "id": 6 + } + +**Parameters:** + +- Tenant and Profile ID (required) +- AllocationIDs: Array of specific allocation IDs to clear (optional - if empty or omitted, all allocations will be cleared) + +**Returns:** + +- Success confirmation +- Error if any specified allocation IDs don't exist + Use Cases --------- diff --git a/ips/apis.go b/ips/apis.go index 62ba0891d..4558f518a 100644 --- a/ips/apis.go +++ b/ips/apis.go @@ -275,7 +275,7 @@ func (s *IPService) V1ReleaseIP(ctx *context.Context, args *utils.CGREvent, repl return nil } -// V1GetIPAllocations returns a resource configuration +// V1GetIPAllocations returns all IP allocations for a tenantID. func (s *IPService) V1GetIPAllocations(ctx *context.Context, arg *utils.TenantIDWithAPIOpts, reply *utils.IPAllocations) error { if missing := utils.MissingStructFields(arg, []string{utils.ID}); len(missing) != 0 { //Params missing return utils.NewErrMandatoryIeMissing(missing...) @@ -298,3 +298,35 @@ func (s *IPService) V1GetIPAllocations(ctx *context.Context, arg *utils.TenantID *reply = *ip return nil } + +// V1ClearIPAllocations clears IP allocations from an IPAllocations object. +// If args.AllocationIDs is empty or nil, all allocations will be cleared. +func (s *IPService) V1ClearIPAllocations(ctx *context.Context, args *utils.ClearIPAllocationsArgs, reply *string) error { + if missing := utils.MissingStructFields(args, []string{utils.ID}); len(missing) != 0 { + return utils.NewErrMandatoryIeMissing(missing...) + } + + tnt := args.Tenant + if tnt == utils.EmptyString { + tnt = s.cfg.GeneralCfg().DefaultTenant + } + + lkID := guardian.Guardian.GuardIDs(utils.EmptyString, + config.CgrConfig().GeneralCfg().LockingTimeout, + utils.IPAllocationsLockKey(tnt, args.ID)) + defer guardian.Guardian.UnguardIDs(lkID) + + allocs, err := s.dm.GetIPAllocations(ctx, tnt, args.ID, true, true, utils.NonTransactional) + if err != nil { + return err + } + if err := allocs.ClearAllocations(args.AllocationIDs); err != nil { + return err + } + if err := s.storeIPAllocations(ctx, allocs); err != nil { + return err + } + + *reply = utils.OK + return nil +} diff --git a/ips/ips_it_test.go b/ips/ips_it_test.go index 99ea2463f..cf7f97684 100644 --- a/ips/ips_it_test.go +++ b/ips/ips_it_test.go @@ -95,26 +95,12 @@ func TestIPsIT(t *testing.T) { "exists_indexed_fields": [], "notexists_indexed_fields": [], "opts":{ - "*allocationID": [ - { - "Tenant": "cgrates.org", - "FilterIDs": ["*string:~*req.Account:1001"], - "Value": "cfg_allocation" - } - ], - // "*ttl": [ - // { - // "Tenant": "*any", - // "FilterIDs": [], - // "Value": "72h" - // } - // ], - // "*units": [ - // { - // "Tenant": "*any", - // "FilterIDs": [], - // "Value": 1 - // } + // "*allocationID": [ + // { + // "Tenant": "cgrates.org", + // "FilterIDs": ["*string:~*req.Account:1001"], + // "Value": "cfg_allocation" + // } // ] } }, @@ -236,6 +222,7 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des ID: "GetIPsForEvent1", Event: map[string]any{ utils.AccountField: "1001", + utils.Destination: "2001", }, APIOpts: map[string]any{ utils.OptsIPsAllocationID: allocID, @@ -251,8 +238,11 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des ID: "AuthorizeIP1", Event: map[string]any{ utils.AccountField: "1001", + utils.Destination: "2001", + }, + APIOpts: map[string]any{ + utils.OptsIPsAllocationID: allocID, }, - APIOpts: map[string]any{}, }, &allocIP); err != nil { t.Error(err) } @@ -263,8 +253,11 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des ID: "AllocateIP1", Event: map[string]any{ utils.AccountField: "1001", + utils.Destination: "2001", + }, + APIOpts: map[string]any{ + utils.OptsIPsAllocationID: allocID, }, - APIOpts: map[string]any{}, }, &allocIP); err != nil { t.Error(err) } @@ -276,8 +269,20 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des ID: "ReleaseIP1", Event: map[string]any{ utils.AccountField: "1001", + utils.Destination: "2001", }, - APIOpts: map[string]any{}, + APIOpts: map[string]any{ + utils.OptsIPsAllocationID: allocID, + }, + }, &reply); err != nil { + t.Error(err) + } + + if err := client.Call(context.Background(), utils.IPsV1ClearIPAllocations, + &utils.ClearIPAllocationsArgs{ + Tenant: "cgrates.org", + ID: "IPs1", + // AllocationIDs: []string{allocID}, }, &reply); err != nil { t.Error(err) } @@ -298,7 +303,7 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des }, Event: map[string]any{ utils.AccountField: "1001", - utils.Destination: "1002", + utils.Destination: "2001", utils.SetupTime: "2018-01-07T17:00:00Z", }, }, &reply); err != nil { @@ -313,7 +318,7 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des }, Event: map[string]any{ utils.AccountField: "1001", - utils.Destination: "1002", + utils.Destination: "2001", utils.SetupTime: "2018-01-07T17:00:00Z", }, }, &reply); err != nil { @@ -321,6 +326,7 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des } }) } + func BenchmarkIPsAuthorize(b *testing.B) { cfg := config.NewDefaultCGRConfig() dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DataDbCfg().Items) diff --git a/utils/consts.go b/utils/consts.go index 415046824..fec21d818 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1679,6 +1679,7 @@ const ( IPsV1AuthorizeIP = "IPsV1.AuthorizeIP" IPsV1AllocateIP = "IPsV1.AllocateIP" IPsV1ReleaseIP = "IPsV1.ReleaseIP" + IPsV1ClearIPAllocations = "IPsV1.ClearIPAllocations" AdminSv1SetIPProfile = "AdminSv1.SetIPProfile" AdminSv1GetIPProfiles = "AdminSv1.GetIPProfiles" AdminSv1RemoveIPProfile = "AdminSv1.RemoveIPProfile" diff --git a/utils/ips.go b/utils/ips.go index 7092c66ab..c02d7c258 100644 --- a/utils/ips.go +++ b/utils/ips.go @@ -416,6 +416,15 @@ type IPAllocationsWithAPIOpts struct { APIOpts map[string]any } +// ClearIPAllocationsArgs contains arguments for clearing IP allocations. +// If AllocationIDs is empty or nil, all allocations will be cleared. +type ClearIPAllocationsArgs struct { + Tenant string + ID string + AllocationIDs []string + APIOpts map[string]any +} + // ComputeUnexported populates lookup maps and profile reference from exported fields. // Must be called after retrieving from DB. func (a *IPAllocations) ComputeUnexported(prfl *IPProfile) error { @@ -459,6 +468,46 @@ func (a *IPAllocations) ReleaseAllocation(allocID string) error { return nil } +// ClearAllocations clears specified IP allocations or all allocations if allocIDs is empty/nil. +// Either all specified IDs exist and get cleared, or none are cleared and an error is returned. +func (a *IPAllocations) ClearAllocations(allocIDs []string) error { + if len(allocIDs) == 0 { + clear(a.Allocations) + clear(a.poolAllocs) + a.TTLIndex = a.TTLIndex[:0] // maintain capacity + return nil + } + + // Validate all IDs exist before clearing any. + var notFound []string + for _, allocID := range allocIDs { + if _, has := a.Allocations[allocID]; !has { + notFound = append(notFound, allocID) + } + } + if len(notFound) > 0 { + return fmt.Errorf("cannot find allocation records with ids: %v", notFound) + } + + for _, allocID := range allocIDs { + alloc := a.Allocations[allocID] + if poolMap, hasPool := a.poolAllocs[alloc.PoolID]; hasPool { + delete(poolMap, alloc.Address) + } + if a.prfl.TTL > 0 { + for i, refID := range a.TTLIndex { + if refID == allocID { + a.TTLIndex = slices.Delete(a.TTLIndex, i, i+1) + break + } + } + } + delete(a.Allocations, allocID) + } + + return nil +} + // AllocateIPOnPool allocates an IP from the specified pool or refreshes // existing allocation. If dryRun is true, checks availability without // allocating.