Add action type *dynamic_resource

This commit is contained in:
arberkatellari
2025-06-11 17:30:58 +02:00
committed by Dan Christian Bogos
parent 36abb6fac8
commit 0e15a7826f
3 changed files with 351 additions and 14 deletions

View File

@@ -131,6 +131,7 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf
utils.MetaDynamicActionPlanAccounts, utils.MetaDynamicAction,
utils.MetaDynamicDestination, utils.MetaDynamicFilter,
utils.MetaDynamicRoute, utils.MetaDynamicRatingProfile,
utils.MetaDynamicResource,
}
act := ActionConnCfg{}
switch source {
@@ -202,6 +203,7 @@ func init() {
actionFuncMap[utils.MetaDynamicRanking] = dynamicRanking
actionFuncMap[utils.MetaDynamicRatingProfile] = dynamicRatingProfile
actionFuncMap[utils.MetaDynamicRanking] = dynamicTrend
actionFuncMap[utils.MetaDynamicResource] = dynamicResource
}
func getActionFunc(typ string) (f actionTypeFunc, exists bool) {
@@ -1005,7 +1007,7 @@ func alterSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, ";")
if len(params) != 5 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 5", len(params))
}
// If conversion fails, limit will default to 0.
@@ -1050,7 +1052,7 @@ func forceDisconnectSessionsAction(_ *Account, act *Action, _ Actions, _ *Filter
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, ";")
if len(params) != 5 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 5", len(params))
}
// If conversion fails, limit will default to 0.
@@ -1484,7 +1486,7 @@ func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 13 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 13", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 13", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -1617,7 +1619,7 @@ func dynamicStats(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 14 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 14", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 14", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -1759,7 +1761,7 @@ func dynamicAttribute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 12 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 12", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 12", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -1868,7 +1870,7 @@ func dynamicActionPlan(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 5 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 5", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -1956,7 +1958,7 @@ func dynamicActionPlanAccount(_ *Account, act *Action, _ Actions, _ *FilterS, ev
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 6 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 6", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 6", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2077,7 +2079,7 @@ func dynamicAction(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
params = append(params, bildr.String()) // append last param left even if empty
// Parse action parameters based on the predefined format.
if len(params) != 17 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 17", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 17", len(params))
}
// replace '&' with ';' before parsing to comply with TPAction fields that need ";" seperators
params[3] = strings.ReplaceAll(params[3], utils.ANDSep, utils.InfieldSep)
@@ -2156,7 +2158,7 @@ func dynamicDestination(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse Destination parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 2 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 2", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 2", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2207,7 +2209,7 @@ func dynamicFilter(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 7 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 7", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 7", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2302,7 +2304,7 @@ func dynamicRoute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 17 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 17", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 17", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2433,7 +2435,7 @@ func dynamicRanking(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 10 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 10", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 10", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2514,7 +2516,7 @@ func dynamicRatingProfile(_ *Account, act *Action, _ Actions, _ *FilterS, ev any
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 7 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 7", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 7", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2586,7 +2588,7 @@ func dynamicTrend(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 13 {
return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 13", len(params)))
return fmt.Errorf("invalid number of parameters <%d> expected 13", len(params))
}
// parse dynamic parameters
for i := range params {
@@ -2658,3 +2660,124 @@ func dynamicTrend(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
var reply string
return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetTrendProfile, trend, &reply)
}
// dynamicResource processes the `ExtraParameters` field from the action to
// construct a ResourceProfile
//
// The ExtraParameters field format is expected as follows:
//
// 0 Tenant: string
// 1 Id: string
// 2 FilterIDs: strings separated by "&".
// 3 ActivationInterval: strings separated by "&".
// 4 TTL: duration
// 5 Limit: float
// 6 AllocationMessage: string
// 7 Blocker: bool
// 8 Stored: bool
// 9 Weight: float
// 10 ThresholdIDs: strings separated by "&".
// 11 APIOpts: set of key-value pairs (separated by "&").
//
// Parameters are separated by ";" and must be provided in the specified order.
func dynamicResource(_ *Account, act *Action, _ Actions, _ *FilterS, ev any,
_ SharedActionsData, connCfg ActionConnCfg) (err error) {
cgrEv, canCast := ev.(*utils.CGREvent)
if !canCast {
return errors.New("Couldn't cast event to CGREvent")
}
dP := utils.MapStorage{ // create DataProvider from event
utils.MetaReq: cgrEv.Event,
utils.MetaTenant: cgrEv.Tenant,
utils.MetaNow: time.Now(),
utils.MetaOpts: cgrEv.APIOpts,
}
// Parse action parameters based on the predefined format.
params := strings.Split(act.ExtraParameters, utils.InfieldSep)
if len(params) != 12 {
return fmt.Errorf("invalid number of parameters <%d> expected 12", len(params))
}
// parse dynamic parameters
for i := range params {
if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil {
return err
}
}
// Prepare request arguments based on provided parameters.
rsc := &ResourceProfileWithAPIOpts{
ResourceProfile: &ResourceProfile{
Tenant: params[0],
ID: params[1],
ActivationInterval: &utils.ActivationInterval{}, // avoid reaching inside a nil pointer
AllocationMessage: params[6],
},
APIOpts: make(map[string]any),
}
// populate Resource's FilterIDs
if params[2] != utils.EmptyString {
rsc.FilterIDs = strings.Split(params[2], utils.ANDSep)
}
// populate Resource's ActivationInterval
aISplit := strings.Split(params[3], utils.ANDSep)
if len(aISplit) > 2 {
return utils.ErrUnsupportedFormat
}
if len(aISplit) > 0 && aISplit[0] != utils.EmptyString {
if err := rsc.ActivationInterval.ActivationTime.UnmarshalText([]byte(aISplit[0])); err != nil {
return err
}
if len(aISplit) == 2 {
if err := rsc.ActivationInterval.ExpiryTime.UnmarshalText([]byte(aISplit[1])); err != nil {
return err
}
}
}
// populate Resource's UsageTTL
if params[4] != utils.EmptyString {
rsc.UsageTTL, err = utils.ParseDurationWithNanosecs(params[4])
if err != nil {
return err
}
}
// populate Resource's Limit
if params[5] != utils.EmptyString {
rsc.Limit, err = strconv.ParseFloat(params[5], 64)
if err != nil {
return err
}
}
// populate Resource's Blocker
if params[7] != utils.EmptyString {
rsc.Blocker, err = strconv.ParseBool(params[7])
if err != nil {
return err
}
}
// populate Resource's Stored
if params[8] != utils.EmptyString {
rsc.Stored, err = strconv.ParseBool(params[8])
if err != nil {
return err
}
}
// populate Resource's Weight
if params[9] != utils.EmptyString {
rsc.Weight, err = strconv.ParseFloat(params[9], 64)
if err != nil {
return err
}
}
// populate Resource's ThresholdIDs
if params[10] != utils.EmptyString {
rsc.ThresholdIDs = strings.Split(params[10], utils.ANDSep)
}
// populate Resource's APIOpts
if params[11] != utils.EmptyString {
if err := parseParamStringToMap(params[11], rsc.APIOpts); err != nil {
return err
}
}
// create the ResourceProfile based on the populated parameters
var reply string
return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetResourceProfile, rsc, &reply)
}

View File

@@ -7121,3 +7121,216 @@ func TestDynamicTrend(t *testing.T) {
})
}
}
func TestDynamicResource(t *testing.T) {
tempConn := connMgr
tmpDm := dm
tmpCache := Cache
defer func() {
config.SetCgrConfig(config.NewDefaultCGRConfig())
SetConnManager(tempConn)
dm = tmpDm
Cache = tmpCache
}()
Cache.Clear(nil)
var rpwo *ResourceProfileWithAPIOpts
ccMock := &ccMock{
calls: map[string]func(ctx *context.Context, args any, reply any) error{
utils.APIerSv1SetResourceProfile: func(ctx *context.Context, args, reply any) error {
var canCast bool
if rpwo, canCast = args.(*ResourceProfileWithAPIOpts); !canCast {
return fmt.Errorf("couldnt cast")
}
return nil
},
},
}
connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier)
clientconn := make(chan birpc.ClientConnector, 1)
clientconn <- ccMock
NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{
connID: clientconn,
})
testcases := []struct {
name string
extraParams string
connIDs []string
expRpwo *ResourceProfileWithAPIOpts
expectedErr string
}{
{
name: "SuccessfulRequest",
connIDs: []string{connID},
expRpwo: &ResourceProfileWithAPIOpts{
ResourceProfile: &ResourceProfile{
Tenant: "cgrates.org",
ID: "RES_ACNT_1001",
FilterIDs: []string{"*string:~*req.Account:1001"},
ActivationInterval: &utils.ActivationInterval{
ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC),
},
UsageTTL: time.Hour,
Limit: 1,
AllocationMessage: "msg_1001",
Blocker: true,
Stored: true,
Weight: 10,
ThresholdIDs: []string{"TD1", "TD2"},
},
APIOpts: map[string]any{
"key": "value",
},
},
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;10;TD1&TD2;key:value",
},
{
name: "SuccessfulRequestWithDynamicPaths",
connIDs: []string{connID},
expRpwo: &ResourceProfileWithAPIOpts{
ResourceProfile: &ResourceProfile{
Tenant: "cgrates.org",
ID: "RES_ACNT_1001",
FilterIDs: []string{"*string:~*req.Account:1001"},
ActivationInterval: &utils.ActivationInterval{
ActivationTime: time.Now(),
ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC),
},
UsageTTL: time.Hour,
Limit: 1,
AllocationMessage: "msg_1001",
Blocker: true,
Stored: true,
Weight: 10,
ThresholdIDs: []string{"TD1", "TD2"},
},
APIOpts: map[string]any{
"key": "value",
},
},
extraParams: "*tenant;RES_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;1h;1;msg_<~*req.Account>;true;true;10;TD1&TD2;key:value",
},
{
name: "SuccessfulRequestEmptyFields",
connIDs: []string{connID},
expRpwo: &ResourceProfileWithAPIOpts{
ResourceProfile: &ResourceProfile{
Tenant: "cgrates.org",
ID: "RES_ACNT_1001",
FilterIDs: nil,
ActivationInterval: &utils.ActivationInterval{},
UsageTTL: 0,
Limit: 0,
AllocationMessage: "",
Blocker: false,
Stored: false,
Weight: 0,
ThresholdIDs: nil,
},
APIOpts: map[string]any{},
},
extraParams: "cgrates.org;RES_ACNT_1001;;;;;;;;;;",
},
{
name: "MissingConns",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;10;TD1&TD2;key:value",
expectedErr: "MANDATORY_IE_MISSING: [connIDs]",
},
{
name: "WrongNumberOfParams",
extraParams: "tenant;RSC;;",
expectedErr: "invalid number of parameters <4> expected 12",
},
{
name: "ActivationIntervalLengthFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z&&;1h;1;msg_1001;true;true;10;TD1&TD2;key:value",
expectedErr: utils.ErrUnsupportedFormat.Error(),
},
{
name: "ActivationIntervalBadStringFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;bad String;1h;1;msg_1001;true;true;10;TD1&TD2;key:value",
expectedErr: `parsing time "bad String" as "2006-01-02T15:04:05Z07:00": cannot parse "bad String" as "2006"`,
},
{
name: "ActivationIntervalBadStringFail2",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z&bad String;1h;1;msg_1001;true;true;10;TD1&TD2;key:value",
expectedErr: `parsing time "bad String" as "2006-01-02T15:04:05Z07:00": cannot parse "bad String" as "2006"`,
},
{
name: "TTLFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;BadString;1;msg_1001;true;true;10;TD1&TD2;key:value",
expectedErr: `time: invalid duration "BadString"`,
},
{
name: "LimitFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;BadString;msg_1001;true;true;10;TD1&TD2;key:value",
expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`,
},
{
name: "BlockerFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;BadString;true;10;TD1&TD2;key:value",
expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`,
},
{
name: "StoredFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;BadString;10;TD1&TD2;key:value",
expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`,
},
{
name: "WeightFail",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;BadString;TD1&TD2;key:value",
expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`,
},
{
name: "InvalidOptsMap",
extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;10;TD1&TD2;opt",
expectedErr: "invalid key-value pair: opt",
},
}
for i, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
action := &Action{ExtraParameters: tc.extraParams}
ev := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "evID",
Time: &time.Time{},
Event: map[string]any{
utils.AccountField: "1001",
},
}
t.Cleanup(func() {
rpwo = nil
})
err := dynamicResource(nil, action, nil, nil, ev,
SharedActionsData{}, ActionConnCfg{
ConnIDs: tc.connIDs,
})
if tc.expectedErr != "" {
if err == nil || err.Error() != tc.expectedErr {
t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err)
}
} else if err != nil {
t.Error(err)
} else if !reflect.DeepEqual(rpwo, tc.expRpwo) {
if i != 1 {
t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo))
} else {
// Get the absolute difference between the times
diff := rpwo.ActivationInterval.ActivationTime.Sub(tc.expRpwo.ActivationInterval.ActivationTime)
if diff < 0 {
diff = -diff // Make sure it's positive
}
// Check if difference is less than or equal to 1 second
if diff <= time.Second {
tc.expRpwo.ActivationInterval.ActivationTime = rpwo.ActivationInterval.ActivationTime
if !reflect.DeepEqual(rpwo, tc.expRpwo) {
t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo))
}
} else {
t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo))
}
}
}
})
}
}

View File

@@ -1240,6 +1240,7 @@ const (
MetaDynamicRanking = "*dynamic_ranking"
MetaDynamicRatingProfile = "*dynamic_rating_profile"
MetaDynamicTrend = "*dynamic_trend"
MetaDynamicResource = "*dynamic_resource"
ActionID = "ActionID"
ActionType = "ActionType"
ActionValue = "ActionValue"