Files
cgrates/engine/ips.go
2025-12-05 13:15:52 +01:00

1042 lines
31 KiB
Go

/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
*/
package engine
import (
"cmp"
"errors"
"fmt"
"maps"
"net/netip"
"runtime"
"slices"
"sync"
"time"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/guardian"
"github.com/cgrates/cgrates/utils"
)
// IPProfile defines the configuration of an IPAllocations object.
type IPProfile struct {
Tenant string
ID string
FilterIDs []string
ActivationInterval *utils.ActivationInterval
Weight float64
TTL time.Duration
Stored bool
Pools []*IPPool
lockID string // reference ID of lock used when matching the IPProfile
}
// IPProfileWithAPIOpts wraps IPProfile with APIOpts.
type IPProfileWithAPIOpts struct {
*IPProfile
APIOpts map[string]any
}
// TenantID returns the concatenated tenant and ID.
func (p *IPProfile) TenantID() string {
return utils.ConcatenatedKey(p.Tenant, p.ID)
}
// Clone creates a deep copy of IPProfile for thread-safe use.
func (p *IPProfile) Clone() *IPProfile {
if p == nil {
return nil
}
pools := make([]*IPPool, 0, len(p.Pools))
for _, pool := range p.Pools {
pools = append(pools, pool.Clone())
}
return &IPProfile{
Tenant: p.Tenant,
ID: p.ID,
FilterIDs: slices.Clone(p.FilterIDs),
ActivationInterval: p.ActivationInterval.Clone(),
Weight: p.Weight,
TTL: p.TTL,
Stored: p.Stored,
Pools: pools,
lockID: p.lockID,
}
}
// CacheClone returns a clone of IPProfile used by ltcache CacheCloner
func (p *IPProfile) CacheClone() any {
return p.Clone()
}
// Lock acquires a guardian lock on the IPProfile and stores the lock ID.
// Uses given lockID or creates a new lock.
func (p *IPProfile) lock(lockID string) {
if lockID == "" {
lockID = guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout,
ipProfileLockKey(p.Tenant, p.ID))
}
p.lockID = lockID
}
// Unlock releases the lock on the IPProfile and clears the stored lock ID.
func (p *IPProfile) unlock() {
if p.lockID == "" {
return
}
// Store current lock ID before clearing to prevent race conditions.
id := p.lockID
p.lockID = ""
guardian.Guardian.UnguardIDs(id)
}
// IPProfileLockKey returns the ID used to lock an IPProfile with guardian.
func ipProfileLockKey(tnt, id string) string {
return utils.ConcatenatedKey(utils.CacheIPProfiles, tnt, id)
}
// IPPool defines a pool of IP addresses within an IPProfile.
type IPPool struct {
ID string
FilterIDs []string
Type string
Range string
Strategy string
Message string
Weight float64
Blocker bool
}
// Clone creates a deep copy of Pool for thread-safe use.
func (p *IPPool) Clone() *IPPool {
if p == nil {
return nil
}
return &IPPool{
ID: p.ID,
FilterIDs: slices.Clone(p.FilterIDs),
Type: p.Type,
Range: p.Range,
Strategy: p.Strategy,
Message: p.Message,
Weight: p.Weight,
Blocker: p.Blocker,
}
}
// PoolAllocation represents one allocation in the pool.
type PoolAllocation struct {
PoolID string // pool ID within the IPProfile
Address netip.Addr // computed IP address
Time time.Time // when this allocation was created
}
// IsActive checks if the allocation is still active.
func (a *PoolAllocation) isActive(ttl time.Duration) bool {
return time.Now().Before(a.Time.Add(ttl))
}
// Clone creates a deep copy of the PoolAllocation object.
func (a *PoolAllocation) Clone() *PoolAllocation {
if a == nil {
return nil
}
clone := *a
return &clone
}
// AllocatedIP represents one IP allocated on a pool, together with the message.
type AllocatedIP struct {
ProfileID string
PoolID string
Message string
Address netip.Addr
}
// AsNavigableMap implements engine.NavigableMapper.
func (ip *AllocatedIP) AsNavigableMap() map[string]*utils.DataNode {
return map[string]*utils.DataNode{
utils.ProfileID: utils.NewLeafNode(ip.ProfileID),
utils.PoolID: utils.NewLeafNode(ip.PoolID),
utils.Message: utils.NewLeafNode(ip.Message),
utils.Address: utils.NewLeafNode(ip.Address.String()),
}
}
// IPAllocations represents IP allocations with usage tracking and TTL management.
type IPAllocations struct {
Tenant string
ID string
Allocations map[string]*PoolAllocation // map[allocID]*PoolAllocation
TTLIndex []string // allocIDs ordered by allocation time for TTL expiry
prfl *IPProfile
poolRanges map[string]netip.Prefix // parsed CIDR ranges by pool ID
poolAllocs map[string]map[netip.Addr]string // IP to allocation ID mapping by pool (map[poolID]map[Addr]allocID)
lockID string
}
// IPAllocationsWithAPIOpts wraps IPAllocations with APIOpts.
type IPAllocationsWithAPIOpts struct {
*IPAllocations
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 sets up unexported fields based on the provided profile.
// Safe to call multiple times with the same profile.
func (a *IPAllocations) computeUnexported(prfl *IPProfile) error {
if prfl == nil {
return nil // nothing to compute without a profile
}
if a.prfl == prfl {
return nil // already computed for this profile
}
a.prfl = prfl
a.poolAllocs = make(map[string]map[netip.Addr]string)
for allocID, alloc := range a.Allocations {
if _, hasPool := a.poolAllocs[alloc.PoolID]; !hasPool {
a.poolAllocs[alloc.PoolID] = make(map[netip.Addr]string)
}
a.poolAllocs[alloc.PoolID][alloc.Address] = allocID
}
a.poolRanges = make(map[string]netip.Prefix)
for _, poolCfg := range a.prfl.Pools {
prefix, err := netip.ParsePrefix(poolCfg.Range)
if err != nil {
return err
}
a.poolRanges[poolCfg.ID] = prefix
}
return nil
}
// releaseAllocation releases the allocation for an ID.
func (a *IPAllocations) releaseAllocation(allocID string) error {
alloc, has := a.Allocations[allocID] // Get the allocation first
if !has {
return fmt.Errorf("cannot find allocation record with id: %s", 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
}
// 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.
func (a *IPAllocations) allocateIPOnPool(allocID string, pool *IPPool,
dryRun bool) (*AllocatedIP, error) {
a.removeExpiredUnits()
if poolAlloc, has := a.Allocations[allocID]; has && !dryRun {
poolAlloc.Time = time.Now()
if a.prfl.TTL > 0 {
a.removeAllocFromTTLIndex(allocID)
}
a.TTLIndex = append(a.TTLIndex, allocID)
return &AllocatedIP{
ProfileID: a.ID,
PoolID: pool.ID,
Message: pool.Message,
Address: poolAlloc.Address,
}, nil
}
poolRange := a.poolRanges[pool.ID]
if !poolRange.IsSingleIP() {
return nil, errors.New("only single IP Pools are supported for now")
}
addr := poolRange.Addr()
if _, hasPool := a.poolAllocs[pool.ID]; hasPool {
if alcID, inUse := a.poolAllocs[pool.ID][addr]; inUse {
return nil, fmt.Errorf("allocation failed for pool %q, IP %q: %w (allocated to %q)",
pool.ID, addr, utils.ErrIPAlreadyAllocated, alcID)
}
}
allocIP := &AllocatedIP{
ProfileID: a.ID,
PoolID: pool.ID,
Message: pool.Message,
Address: addr,
}
if dryRun {
return allocIP, nil
}
a.Allocations[allocID] = &PoolAllocation{
PoolID: pool.ID,
Address: addr,
Time: time.Now(),
}
if _, hasPool := a.poolAllocs[pool.ID]; !hasPool {
a.poolAllocs[pool.ID] = make(map[netip.Addr]string)
}
a.poolAllocs[pool.ID][addr] = allocID
return allocIP, nil
}
// removeExpiredUnits removes expired allocations.
// It stops at first active since TTLIndex is sorted by expiration.
func (a *IPAllocations) removeExpiredUnits() {
expiredCount := 0
for _, allocID := range a.TTLIndex {
alloc, exists := a.Allocations[allocID]
if exists && alloc.isActive(a.prfl.TTL) {
break
}
if alloc != nil {
if poolMap, hasPool := a.poolAllocs[alloc.PoolID]; hasPool {
delete(poolMap, alloc.Address)
}
}
delete(a.Allocations, allocID)
expiredCount++
}
if expiredCount > 0 {
a.TTLIndex = a.TTLIndex[expiredCount:]
}
}
// removeAllocFromTTLIndex removes an allocationID from TTL index.
func (a *IPAllocations) removeAllocFromTTLIndex(allocID string) {
for i, alID := range a.TTLIndex {
if alID == allocID {
a.TTLIndex = slices.Delete(a.TTLIndex, i, i+1)
break
}
}
}
// lock acquires a guardian lock on the IPAllocations and stores the lock ID.
// Uses given lockID (assumes already acquired) or creates a new lock.
func (a *IPAllocations) lock(lockID string) {
if lockID == "" {
lockID = guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout,
ipAllocationsLockKey(a.Tenant, a.ID))
}
a.lockID = lockID
}
// unlock releases the lock on the IPAllocations and clears the stored lock ID.
func (a *IPAllocations) unlock() {
if a.lockID == "" {
return
}
// Store current lock ID before clearing to prevent race conditions.
id := a.lockID
a.lockID = ""
guardian.Guardian.UnguardIDs(id)
if a.prfl != nil {
a.prfl.unlock()
}
}
// config returns the IPAllocations' profile configuration.
func (a *IPAllocations) config() *IPProfile {
return a.prfl
}
// TenantID returns the unique ID in a multi-tenant environment
func (a *IPAllocations) TenantID() string {
return utils.ConcatenatedKey(a.Tenant, a.ID)
}
// CacheClone returns a clone of IPAllocations object used by ltcache CacheCloner.
func (a *IPAllocations) CacheClone() any {
return a.Clone()
}
// Clone creates a deep clone of the IPAllocations object (lockID excluded).
func (a *IPAllocations) Clone() *IPAllocations {
if a == nil {
return nil
}
clone := &IPAllocations{
Tenant: a.Tenant,
ID: a.ID,
TTLIndex: slices.Clone(a.TTLIndex),
prfl: a.prfl.Clone(),
poolRanges: maps.Clone(a.poolRanges),
}
if a.poolAllocs != nil {
clone.poolAllocs = make(map[string]map[netip.Addr]string)
for poolID, allocs := range a.poolAllocs {
clone.poolAllocs[poolID] = maps.Clone(allocs)
}
}
if a.Allocations != nil {
clone.Allocations = make(map[string]*PoolAllocation, len(a.Allocations))
for id, alloc := range a.Allocations {
clone.Allocations[id] = alloc.Clone()
}
}
return clone
}
// IPAllocationsLockKey builds the guardian key for locking IP allocations.
func ipAllocationsLockKey(tnt, id string) string {
return utils.ConcatenatedKey(utils.CacheIPAllocations, tnt, id)
}
// IPService is the service handling IP allocations
type IPService struct {
cfg *config.CGRConfig
dm *DataManager // So we can load the data in cache and index it
cm *ConnManager
fltrs *FilterS
storedIPsMux sync.RWMutex // protects storedIPs
storedIPs utils.StringSet // keep a record of IP allocations which need saving, map[allocsID]bool
stopBackup chan struct{} // control storing process
loopStopped chan struct{}
}
// NewIPService returns a new IPService.
func NewIPService(dm *DataManager, cfg *config.CGRConfig, fltrs *FilterS,
cm *ConnManager) *IPService {
return &IPService{dm: dm,
storedIPs: make(utils.StringSet),
cfg: cfg,
cm: cm,
fltrs: fltrs,
loopStopped: make(chan struct{}),
stopBackup: make(chan struct{}),
}
}
// Reload restarts the backup loop.
func (s *IPService) Reload() {
close(s.stopBackup)
<-s.loopStopped // wait until the loop is done
s.stopBackup = make(chan struct{})
go s.runBackup()
}
// StartLoop starts the gorutine with the backup loop
func (s *IPService) StartLoop() {
go s.runBackup()
}
// Shutdown is called to shutdown the service
func (s *IPService) Shutdown() {
close(s.stopBackup)
s.storeIPAllocationsList()
}
// runBackup will regularly update IP allocations stored in dataDB.
func (s *IPService) runBackup() {
storeInterval := s.cfg.IPsCfg().StoreInterval
if storeInterval <= 0 {
s.loopStopped <- struct{}{}
return
}
for {
s.storeIPAllocationsList()
select {
case <-s.stopBackup:
s.loopStopped <- struct{}{}
return
case <-time.After(storeInterval):
}
}
}
// storeIPAllocationsList represents one task of complete backup
func (s *IPService) storeIPAllocationsList() {
var failedAllocIDs []string
for { // don't stop until we store all dirty IP allocations
s.storedIPsMux.Lock()
allocsID := s.storedIPs.GetOne()
if allocsID != "" {
s.storedIPs.Remove(allocsID)
}
s.storedIPsMux.Unlock()
if allocsID == "" {
break // no more keys, backup completed
}
allocIf, ok := Cache.Get(utils.CacheIPAllocations, allocsID)
if !ok || allocIf == nil {
utils.Logger.Warning(fmt.Sprintf(
"<%s> failed retrieving from cache IP allocations with ID %q", utils.IPs, allocsID))
continue
}
allocs := allocIf.(*IPAllocations)
allocs.lock(utils.EmptyString)
if err := s.storeIPAllocations(allocs); err != nil {
utils.Logger.Warning(fmt.Sprintf("<%s> %v", utils.IPs, err))
failedAllocIDs = append(failedAllocIDs, allocsID) // record failure so we can schedule it for next backup
}
allocs.unlock()
// randomize the CPU load and give up thread control
runtime.Gosched()
}
if len(failedAllocIDs) != 0 { // there were errors on save, schedule the keys for next backup
s.storedIPsMux.Lock()
s.storedIPs.AddSlice(failedAllocIDs)
s.storedIPsMux.Unlock()
}
}
// storeIPAllocations stores the IP allocations in DB.
func (s *IPService) storeIPAllocations(allocs *IPAllocations) error {
if err := s.dm.SetIPAllocations(allocs); err != nil {
utils.Logger.Warning(fmt.Sprintf(
"<IPs> could not save IP allocations %q: %v", allocs.ID, err))
return err
}
//since we no longer handle cache in DataManager do here a manual caching
if tntID := allocs.TenantID(); Cache.HasItem(utils.CacheIPAllocations, tntID) { // only cache if previously there
if err := Cache.Set(utils.CacheIPAllocations, tntID, allocs, nil,
true, utils.NonTransactional); err != nil {
utils.Logger.Warning(fmt.Sprintf(
"<IPs> could not cache IP allocations %q: %v", tntID, err))
return err
}
}
return nil
}
// storeMatchedIPAllocations will store the list of IP allocations based on the StoreInterval.
func (s *IPService) storeMatchedIPAllocations(matched *IPAllocations) error {
if s.cfg.IPsCfg().StoreInterval == 0 {
return nil
}
if s.cfg.IPsCfg().StoreInterval > 0 {
s.storedIPsMux.Lock()
s.storedIPs.Add(matched.TenantID())
s.storedIPsMux.Unlock()
return nil
}
if err := s.storeIPAllocations(matched); err != nil {
return err
}
return nil
}
// matchingIPAllocationsForEvent returns the IP allocation with the highest weight
// matching the event.
func (s *IPService) matchingIPAllocationsForEvent(tnt string,
ev *utils.CGREvent, evUUID string) (allocs *IPAllocations, err error) {
evNm := utils.MapStorage{
utils.MetaReq: ev.Event,
utils.MetaOpts: ev.APIOpts,
}
var itemIDs []string
if x, ok := Cache.Get(utils.CacheEventIPs, evUUID); ok {
// IPIDs cached as utils.StringSet{"resID":bool}
if x == nil {
return nil, utils.ErrNotFound
}
itemIDs = []string{x.(string)}
defer func() { // make sure we uncache if we find errors
if err != nil {
// TODO: Consider using RemoveWithoutReplicate instead, as
// partitions with Replicate=true call ReplicateRemove in
// onEvict by default.
if errCh := Cache.Remove(utils.CacheEventIPs, evUUID,
true, utils.NonTransactional); errCh != nil {
err = errCh
}
}
}()
} else { // select the IP allocation IDs out of dataDB
matchedItemIDs, err := MatchingItemIDsForEvent(evNm,
s.cfg.IPsCfg().StringIndexedFields,
s.cfg.IPsCfg().PrefixIndexedFields,
s.cfg.IPsCfg().SuffixIndexedFields,
s.cfg.IPsCfg().ExistsIndexedFields,
s.dm, utils.CacheIPFilterIndexes, tnt,
s.cfg.IPsCfg().IndexedSelects,
s.cfg.IPsCfg().NestedFields,
)
if err != nil {
if err == utils.ErrNotFound {
if errCh := Cache.Set(utils.CacheEventIPs, evUUID,
nil, nil, true, ""); errCh != nil { // cache negative match
return nil, errCh
}
}
return nil, err
}
itemIDs = slices.Sorted(maps.Keys(matchedItemIDs))
}
var matchedPrfl *IPProfile
var maxWeight float64
for _, id := range itemIDs {
lkPrflID := guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout,
ipProfileLockKey(tnt, id))
var prfl *IPProfile
if prfl, err = s.dm.GetIPProfile(tnt, id, true, true, utils.NonTransactional); err != nil {
guardian.Guardian.UnguardIDs(lkPrflID)
if err == utils.ErrNotFound {
continue
}
return nil, err
}
prfl.lock(lkPrflID)
if prfl.ActivationInterval != nil && ev.Time != nil &&
!prfl.ActivationInterval.IsActiveAtTime(*ev.Time) { // not active
prfl.unlock()
continue
}
var pass bool
if pass, err = s.fltrs.Pass(tnt, prfl.FilterIDs, evNm); err != nil {
prfl.unlock()
return nil, err
} else if !pass {
prfl.unlock()
continue
}
if matchedPrfl == nil || maxWeight < prfl.Weight {
if matchedPrfl != nil {
matchedPrfl.unlock()
}
matchedPrfl = prfl
maxWeight = prfl.Weight
} else {
prfl.unlock()
}
}
if matchedPrfl == nil {
return nil, utils.ErrNotFound
}
lkID := guardian.Guardian.GuardIDs(utils.EmptyString,
config.CgrConfig().GeneralCfg().LockingTimeout,
ipAllocationsLockKey(matchedPrfl.Tenant, matchedPrfl.ID))
allocs, err = s.dm.GetIPAllocations(matchedPrfl.Tenant, matchedPrfl.ID, true, true, "", matchedPrfl)
if err != nil {
guardian.Guardian.UnguardIDs(lkID)
matchedPrfl.unlock()
return nil, err
}
allocs.lock(lkID)
if err = Cache.Set(utils.CacheEventIPs, evUUID, allocs.ID, nil, true, ""); err != nil {
allocs.unlock()
}
return allocs, nil
}
// allocateFromPools attempts IP allocation across all pools in priority order.
// Continues to next pool only if current pool returns ErrIPAlreadyAllocated.
// Returns first successful allocation or the last allocation error.
func (s *IPService) allocateFromPools(allocs *IPAllocations, allocID string,
poolIDs []string, dryRun bool) (*AllocatedIP, error) {
var err error
for _, poolID := range poolIDs {
pool := findPoolByID(allocs.config().Pools, poolID)
if pool == nil {
return nil, fmt.Errorf("pool %q: %w", poolID, utils.ErrNotFound)
}
var result *AllocatedIP
if result, err = allocs.allocateIPOnPool(allocID, pool, dryRun); err == nil {
return result, nil
}
if !errors.Is(err, utils.ErrIPAlreadyAllocated) {
return nil, err
}
}
return nil, err
}
func findPoolByID(pools []*IPPool, id string) *IPPool {
for _, pool := range pools {
if pool.ID == id {
return pool
}
}
return nil
}
// filterAndSortPools filters pools by their FilterIDs, sorts by weight
// (highest first), and truncates at the first blocking pool.
// TODO: check whether pre-allocating filteredPools & poolIDs improves
// performance or wastes memory when filtering is aggressive.
func filterAndSortPools(tenant string, pools []*IPPool,
fltrs *FilterS, ev utils.DataProvider) ([]string, error) {
var filteredPools []*IPPool
for _, pool := range pools {
pass, err := fltrs.Pass(tenant, pool.FilterIDs, ev)
if err != nil {
return nil, err
}
if !pass {
continue
}
filteredPools = append(filteredPools, pool)
}
if len(filteredPools) == 0 {
return nil, utils.ErrNotFound
}
// Sort by weight (higher values first).
slices.SortFunc(filteredPools, func(a, b *IPPool) int {
return cmp.Compare(b.Weight, a.Weight)
})
var poolIDs []string
for _, pool := range filteredPools {
poolIDs = append(poolIDs, pool.ID)
if pool.Blocker {
break
}
}
return poolIDs, nil
}
// V1GetIPAllocationForEvent returns the IPAllocations object matching the event.
func (s *IPService) V1GetIPAllocationForEvent(ctx *context.Context, args *utils.CGREvent, reply *IPAllocations) (err error) {
if args == nil {
return utils.NewErrMandatoryIeMissing(utils.Event)
}
if missing := utils.MissingStructFields(args, []string{utils.ID, utils.Event}); len(missing) != 0 { //Params missing
return utils.NewErrMandatoryIeMissing(missing...)
}
allocID := utils.GetStringOpts(args, s.cfg.IPsCfg().Opts.AllocationID, utils.OptsIPsAllocationID)
if allocID == utils.EmptyString {
return utils.NewErrMandatoryIeMissing(utils.AllocationID)
}
tnt := args.Tenant
if tnt == utils.EmptyString {
tnt = s.cfg.GeneralCfg().DefaultTenant
}
// RPC caching
if config.CgrConfig().CacheCfg().Partitions[utils.CacheRPCResponses].Limit != 0 {
cacheKey := utils.ConcatenatedKey(utils.IPsV1GetIPAllocationForEvent, utils.ConcatenatedKey(tnt, args.ID))
refID := guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout, cacheKey) // RPC caching needs to be atomic
defer guardian.Guardian.UnguardIDs(refID)
if itm, has := Cache.Get(utils.CacheRPCResponses, cacheKey); has {
cachedResp := itm.(*utils.CachedRPCResponse)
if cachedResp.Error == nil {
*reply = *cachedResp.Result.(*IPAllocations)
}
return cachedResp.Error
}
defer Cache.Set(utils.CacheRPCResponses, cacheKey,
&utils.CachedRPCResponse{Result: reply, Error: err},
nil, true, utils.NonTransactional)
}
// end of RPC caching
var allocs *IPAllocations
if allocs, err = s.matchingIPAllocationsForEvent(tnt, args, allocID); err != nil {
return err
}
defer allocs.unlock()
*reply = *allocs
return
}
// V1AuthorizeIP checks if it's able to allocate an IP address for the given event.
func (s *IPService) V1AuthorizeIP(ctx *context.Context, args *utils.CGREvent, reply *AllocatedIP) (err error) {
if args == nil {
return utils.NewErrMandatoryIeMissing(utils.Event)
}
if missing := utils.MissingStructFields(args, []string{utils.ID, utils.Event}); len(missing) != 0 { //Params missing
return utils.NewErrMandatoryIeMissing(missing...)
}
allocID := utils.GetStringOpts(args, s.cfg.IPsCfg().Opts.AllocationID, utils.OptsIPsAllocationID)
if allocID == utils.EmptyString {
return utils.NewErrMandatoryIeMissing(utils.AllocationID)
}
tnt := args.Tenant
if tnt == utils.EmptyString {
tnt = s.cfg.GeneralCfg().DefaultTenant
}
// RPC caching
if config.CgrConfig().CacheCfg().Partitions[utils.CacheRPCResponses].Limit != 0 {
cacheKey := utils.ConcatenatedKey(utils.IPsV1AuthorizeIP, utils.ConcatenatedKey(tnt, args.ID))
refID := guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout, cacheKey) // RPC caching needs to be atomic
defer guardian.Guardian.UnguardIDs(refID)
if itm, has := Cache.Get(utils.CacheRPCResponses, cacheKey); has {
cachedResp := itm.(*utils.CachedRPCResponse)
if cachedResp.Error == nil {
*reply = *cachedResp.Result.(*AllocatedIP)
}
return cachedResp.Error
}
defer Cache.Set(utils.CacheRPCResponses, cacheKey,
&utils.CachedRPCResponse{Result: reply, Error: err},
nil, true, utils.NonTransactional)
}
// end of RPC caching
var allocs *IPAllocations
if allocs, err = s.matchingIPAllocationsForEvent(tnt, args, allocID); err != nil {
return err
}
defer allocs.unlock()
var poolIDs []string
if poolIDs, err = filterAndSortPools(tnt, allocs.config().Pools, s.fltrs,
args.AsDataProvider()); err != nil {
return err
}
var allocIP *AllocatedIP
if allocIP, err = s.allocateFromPools(allocs, allocID, poolIDs, true); err != nil {
if errors.Is(err, utils.ErrIPAlreadyAllocated) {
return utils.ErrIPUnauthorized
}
return err
}
*reply = *allocIP
return nil
}
// V1AllocateIP allocates an IP address for the given event.
func (s *IPService) V1AllocateIP(ctx *context.Context, args *utils.CGREvent, reply *AllocatedIP) (err error) {
if args == nil {
return utils.NewErrMandatoryIeMissing(utils.Event)
}
if missing := utils.MissingStructFields(args, []string{utils.ID, utils.Event}); len(missing) != 0 { //Params missing
return utils.NewErrMandatoryIeMissing(missing...)
}
allocID := utils.GetStringOpts(args, s.cfg.IPsCfg().Opts.AllocationID, utils.OptsIPsAllocationID)
if allocID == utils.EmptyString {
return utils.NewErrMandatoryIeMissing(utils.AllocationID)
}
tnt := args.Tenant
if tnt == utils.EmptyString {
tnt = s.cfg.GeneralCfg().DefaultTenant
}
// RPC caching
if config.CgrConfig().CacheCfg().Partitions[utils.CacheRPCResponses].Limit != 0 {
cacheKey := utils.ConcatenatedKey(utils.IPsV1AllocateIP, utils.ConcatenatedKey(tnt, args.ID))
refID := guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout, cacheKey) // RPC caching needs to be atomic
defer guardian.Guardian.UnguardIDs(refID)
if itm, has := Cache.Get(utils.CacheRPCResponses, cacheKey); has {
cachedResp := itm.(*utils.CachedRPCResponse)
if cachedResp.Error == nil {
*reply = *cachedResp.Result.(*AllocatedIP)
}
return cachedResp.Error
}
defer Cache.Set(utils.CacheRPCResponses, cacheKey,
&utils.CachedRPCResponse{Result: reply, Error: err},
nil, true, utils.NonTransactional)
}
// end of RPC caching
var allocs *IPAllocations
if allocs, err = s.matchingIPAllocationsForEvent(tnt, args, allocID); err != nil {
return err
}
defer allocs.unlock()
var poolIDs []string
if poolIDs, err = filterAndSortPools(tnt, allocs.config().Pools, s.fltrs,
args.AsDataProvider()); err != nil {
return err
}
var allocIP *AllocatedIP
if allocIP, err = s.allocateFromPools(allocs, allocID, poolIDs, false); err != nil {
return err
}
// index it for storing
if err = s.storeMatchedIPAllocations(allocs); err != nil {
return err
}
*reply = *allocIP
return nil
}
// V1ReleaseIP releases an allocated IP address for the given event.
func (s *IPService) V1ReleaseIP(ctx *context.Context, args *utils.CGREvent, reply *string) (err error) {
if args == nil {
return utils.NewErrMandatoryIeMissing(utils.Event)
}
if missing := utils.MissingStructFields(args, []string{utils.ID, utils.Event}); len(missing) != 0 { //Params missing
return utils.NewErrMandatoryIeMissing(missing...)
}
allocID := utils.GetStringOpts(args, s.cfg.IPsCfg().Opts.AllocationID, utils.OptsIPsAllocationID)
if allocID == utils.EmptyString {
return utils.NewErrMandatoryIeMissing(utils.AllocationID)
}
tnt := args.Tenant
if tnt == utils.EmptyString {
tnt = s.cfg.GeneralCfg().DefaultTenant
}
// RPC caching
if config.CgrConfig().CacheCfg().Partitions[utils.CacheRPCResponses].Limit != 0 {
cacheKey := utils.ConcatenatedKey(utils.IPsV1ReleaseIP, utils.ConcatenatedKey(tnt, args.ID))
refID := guardian.Guardian.GuardIDs("",
config.CgrConfig().GeneralCfg().LockingTimeout, cacheKey) // RPC caching needs to be atomic
defer guardian.Guardian.UnguardIDs(refID)
if itm, has := Cache.Get(utils.CacheRPCResponses, cacheKey); has {
cachedResp := itm.(*utils.CachedRPCResponse)
if cachedResp.Error == nil {
*reply = *cachedResp.Result.(*string)
}
return cachedResp.Error
}
defer Cache.Set(utils.CacheRPCResponses, cacheKey,
&utils.CachedRPCResponse{Result: reply, Error: err},
nil, true, utils.NonTransactional)
}
// end of RPC caching
var allocs *IPAllocations
if allocs, err = s.matchingIPAllocationsForEvent(tnt, args, allocID); err != nil {
return err
}
defer allocs.unlock()
if err = allocs.releaseAllocation(allocID); err != nil {
utils.Logger.Warning(fmt.Sprintf(
"<%s> failed to remove allocation from IPAllocations with ID %q: %v", utils.IPs, allocs.TenantID(), err))
}
// Handle storing
if err = s.storeMatchedIPAllocations(allocs); err != nil {
return err
}
*reply = utils.OK
return nil
}
// V1GetIPAllocations returns all IP allocations for a tenantID.
func (s *IPService) V1GetIPAllocations(ctx *context.Context, arg *utils.TenantIDWithAPIOpts, reply *IPAllocations) error {
if missing := utils.MissingStructFields(arg, []string{utils.ID}); len(missing) != 0 { //Params missing
return utils.NewErrMandatoryIeMissing(missing...)
}
tnt := arg.Tenant
if tnt == utils.EmptyString {
tnt = s.cfg.GeneralCfg().DefaultTenant
}
// make sure resource is locked at process level
lkID := guardian.Guardian.GuardIDs(utils.EmptyString,
config.CgrConfig().GeneralCfg().LockingTimeout,
ipAllocationsLockKey(tnt, arg.ID))
defer guardian.Guardian.UnguardIDs(lkID)
ip, err := s.dm.GetIPAllocations(tnt, arg.ID, true, true, utils.NonTransactional, nil)
if err != nil {
return err
}
*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 *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,
ipAllocationsLockKey(tnt, args.ID))
defer guardian.Guardian.UnguardIDs(lkID)
allocs, err := s.dm.GetIPAllocations(tnt, args.ID, true, true, utils.NonTransactional, nil)
if err != nil {
return err
}
if err := allocs.clearAllocations(args.AllocationIDs); err != nil {
return err
}
if err := s.storeIPAllocations(allocs); err != nil {
return err
}
*reply = utils.OK
return nil
}