diff --git a/engine/action.go b/engine/action.go index a6f343092..b8bd618ba 100644 --- a/engine/action.go +++ b/engine/action.go @@ -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) +} diff --git a/engine/actions_test.go b/engine/actions_test.go index a05190f32..d85acad66 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -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)) + } + } + } + }) + } +} diff --git a/utils/consts.go b/utils/consts.go index 5019e871a..e30e2d9f4 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1240,6 +1240,7 @@ const ( MetaDynamicRanking = "*dynamic_ranking" MetaDynamicRatingProfile = "*dynamic_rating_profile" MetaDynamicTrend = "*dynamic_trend" + MetaDynamicResource = "*dynamic_resource" ActionID = "ActionID" ActionType = "ActionType" ActionValue = "ActionValue"