diff --git a/actions/dynamic.go b/actions/dynamic.go index eb16e0bc8..805900b56 100644 --- a/actions/dynamic.go +++ b/actions/dynamic.go @@ -363,3 +363,148 @@ func (aL *actDynamicStats) execute(ctx *context.Context, data utils.MapStorage, return aL.connMgr.Call(ctx, aL.config.ActionSCfg().AdminSConns, utils.AdminSv1SetStatQueueProfile, args, &rply) } + +// actDynamicAttribute processes the `ActionValue` field from the action to construct a AttributeProfile +// +// The ActionValue field format is expected as follows: +// +// 0 Tenant: string +// 1 ID: string +// 2 FilterIDs: strings separated by "&". +// 3 Weights: strings separated by "&". +// 4 Blocker: strings separated by "&". +// 5 AttributeFilterIDs: strings separated by "&". +// 6 AttributeBlockers: strings separated by "&". +// 7 Path: string +// 8 Type: string +// 9 Value: strings separated by "&". +// 10 APIOpts: set of key-value pairs (separated by "&"). +// +// Parameters are separated by ";" and must be provided in the specified order. +type actDynamicAttribute struct { + config *config.CGRConfig + connMgr *engine.ConnManager + aCfg *utils.APAction + tnt string + cgrEv *utils.CGREvent +} + +func (aL *actDynamicAttribute) id() string { + return aL.aCfg.ID +} + +func (aL *actDynamicAttribute) cfg() *utils.APAction { + return aL.aCfg +} + +// execute implements actioner interface +func (aL *actDynamicAttribute) execute(ctx *context.Context, data utils.MapStorage, trgID string) (err error) { + if len(aL.config.ActionSCfg().AdminSConns) == 0 { + return fmt.Errorf("no connection with AdminS") + } + data[utils.MetaNow] = time.Now() + data[utils.MetaTenant] = utils.FirstNonEmpty(aL.cgrEv.Tenant, aL.tnt, + config.CgrConfig().GeneralCfg().DefaultTenant) + // Parse action parameters based on the predefined format. + if len(aL.aCfg.Diktats) == 0 { + return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID) + } + params := strings.Split(aL.aCfg.Diktats[0].Value, utils.InfieldSep) + if len(params) != 11 { + return fmt.Errorf("invalid number of parameters <%d> expected 11", len(params)) + } + // parse dynamic parameters + for i := range params { + if params[i], err = utils.ParseParamForDataProvider(params[i], data, false); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + args := &utils.APIAttributeProfileWithAPIOpts{ + APIAttributeProfile: &utils.APIAttributeProfile{ + Tenant: params[0], + ID: params[1], + }, + APIOpts: make(map[string]any), + } + // populate Attribute's FilterIDs + if params[2] != utils.EmptyString { + args.FilterIDs = strings.Split(params[2], utils.ANDSep) + } + // populate Attribute's Weights + if params[3] != utils.EmptyString { + args.Weights = utils.DynamicWeights{&utils.DynamicWeight{}} + wghtSplit := strings.Split(params[3], utils.ANDSep) + if len(wghtSplit) > 2 { + return utils.ErrUnsupportedFormat + } + if wghtSplit[0] != utils.EmptyString { + args.Weights[0].FilterIDs = []string{wghtSplit[0]} + } + if wghtSplit[1] != utils.EmptyString { + args.Weights[0].Weight, err = strconv.ParseFloat(wghtSplit[1], 64) + if err != nil { + return err + } + } + } + // populate Attribute's Blockers + if params[4] != utils.EmptyString { + args.Blockers = utils.DynamicBlockers{&utils.DynamicBlocker{}} + blckrSplit := strings.Split(params[4], utils.ANDSep) + if len(blckrSplit) > 2 { + return utils.ErrUnsupportedFormat + } + if blckrSplit[0] != utils.EmptyString { + args.Blockers[0].FilterIDs = []string{blckrSplit[0]} + } + if blckrSplit[1] != utils.EmptyString { + args.Blockers[0].Blocker, err = strconv.ParseBool(blckrSplit[1]) + if err != nil { + return err + } + } + } + // populate Attribute's Attributes + if params[7] != utils.EmptyString { + var attrFltrIDs []string + if params[5] != utils.EmptyString { + attrFltrIDs = strings.Split(params[5], utils.ANDSep) + } + var attrFltrBlckrs utils.DynamicBlockers + if params[6] != utils.EmptyString { + attrFltrBlckrs = utils.DynamicBlockers{&utils.DynamicBlocker{}} + blckrSplit := strings.Split(params[6], utils.ANDSep) + if len(blckrSplit) > 2 { + return utils.ErrUnsupportedFormat + } + if blckrSplit[0] != utils.EmptyString { + attrFltrBlckrs[0].FilterIDs = []string{blckrSplit[0]} + } + if blckrSplit[1] != utils.EmptyString { + attrFltrBlckrs[0].Blocker, err = strconv.ParseBool(blckrSplit[1]) + if err != nil { + return err + } + } + } + args.Attributes = append(args.Attributes, &utils.ExternalAttribute{ + FilterIDs: attrFltrIDs, + Blockers: attrFltrBlckrs, + Path: params[7], + Type: params[8], + Value: params[9], + }) + } + // populate Attribute's APIOpts + if params[10] != utils.EmptyString { + if err := parseParamStringToMap(params[10], args.APIOpts); err != nil { + return err + } + } + + // create the AttributeProfile based on the populated parameters + var rply string + return aL.connMgr.Call(ctx, aL.config.ActionSCfg().AdminSConns, + utils.AdminSv1SetAttributeProfile, args, &rply) +} diff --git a/actions/libactions.go b/actions/libactions.go index ebd02b080..bf72fdeb3 100644 --- a/actions/libactions.go +++ b/actions/libactions.go @@ -37,6 +37,8 @@ func actionTarget(act string) string { return utils.MetaThresholds case utils.MetaAddBalance, utils.MetaSetBalance, utils.MetaRemBalance: return utils.MetaAccounts + case utils.MetaDynamicAttribute: + return utils.MetaAttributes default: return utils.MetaNone } @@ -137,6 +139,8 @@ func newActioner(ctx *context.Context, cgrEv *utils.CGREvent, cfg *config.CGRCon return &actDynamicThreshold{cfg, connMgr, aCfg, tnt, cgrEv}, nil case utils.MetaDynamicStats: return &actDynamicStats{cfg, connMgr, aCfg, tnt, cgrEv}, nil + case utils.MetaDynamicAttribute: + return &actDynamicAttribute{cfg, connMgr, aCfg, tnt, cgrEv}, nil default: return nil, fmt.Errorf("unsupported action type: <%s>", aCfg.Type) diff --git a/general_tests/dynamic_thresholds_it_test.go b/general_tests/dynamic_thresholds_it_test.go index 1d6e8884b..05d4c11ec 100644 --- a/general_tests/dynamic_thresholds_it_test.go +++ b/general_tests/dynamic_thresholds_it_test.go @@ -168,9 +168,11 @@ func TestDynThdIT(t *testing.T) { Targets: map[string]utils.StringSet{ utils.MetaThresholds: {"someID": {}}, utils.MetaStats: {"someID": {}}, + utils.MetaAttributes: {"someID": {}}, }, Actions: []*utils.APAction{ { + ID: "Dynamic_Threshold_ID", Type: utils.MetaDynamicThreshold, Diktats: []*utils.APDiktat{ { @@ -180,6 +182,7 @@ func TestDynThdIT(t *testing.T) { }, }, { + ID: "Dynamic_Stats_ID", Type: utils.MetaDynamicStats, Diktats: []*utils.APDiktat{ { @@ -188,6 +191,16 @@ func TestDynThdIT(t *testing.T) { }, }, }, + { + ID: "Dynamic_Attribute_ID", + Type: utils.MetaDynamicAttribute, + Diktats: []*utils.APDiktat{ + { + Path: "ExtraParameters", + Value: "*tenant;DYNAMICLY_ATTR_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&30;*string:~*req.Account:<~*req.Account>&true;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&true;*req.Subject;*constant;SUPPLIER1;~*opts", + }, + }, + }, }, }, } @@ -388,7 +401,8 @@ func TestDynThdIT(t *testing.T) { if err := client.Call(context.Background(), utils.AdminSv1GetStatQueueProfile, &utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: utils.CGRateSorg, - ID: "DYNAMICLY_STAT_1002"}, + ID: "DYNAMICLY_STAT_1002", + }, }, &rply); err != nil { t.Error(err) } else if !reflect.DeepEqual(exp, rply) { @@ -396,4 +410,55 @@ func TestDynThdIT(t *testing.T) { } }) + t.Run("GetDynamicAttributeProfile", func(t *testing.T) { + var attrs []*utils.APIAttributeProfile + if err := client.Call(context.Background(), utils.AdminSv1GetAttributeProfiles, + &utils.ArgsItemIDs{ + Tenant: utils.CGRateSorg, + }, &attrs); err != nil { + t.Errorf("AdminSv1GetAttributeProfiles failed unexpectedly: %v", err) + } + if len(attrs) != 1 { + t.Fatalf("AdminSv1GetAttributeProfiles len(attrs)=%v, want 1", len(attrs)) + } + + exp := []*utils.APIAttributeProfile{ + { + Tenant: utils.CGRateSorg, + ID: "DYNAMICLY_ATTR_1002", + FilterIDs: []string{"*string:~*req.Account:1002"}, + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{"*string:~*req.Account:1002"}, + Weight: 30, + }, + }, + Blockers: utils.DynamicBlockers{ + { + FilterIDs: []string{"*string:~*req.Account:1002"}, + Blocker: true, + }, + }, + Attributes: []*utils.ExternalAttribute{ + { + FilterIDs: []string{"*string:~*req.Account:1002"}, + Blockers: utils.DynamicBlockers{ + { + FilterIDs: []string{"*string:~*req.Account:1002"}, + Blocker: true, + }, + }, + Path: "*req.Subject", + Type: "*constant", + Value: "SUPPLIER1", + }, + }, + }, + } + + if !reflect.DeepEqual(exp, attrs) { + t.Errorf("Expected attributes to be the same. Before shutdown \n<%v>\nAfter rebooting <%v>", utils.ToJSON(attrs), utils.ToJSON(exp)) + } + }) + } diff --git a/utils/consts.go b/utils/consts.go index e120ce60e..56c3d9ce2 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1132,6 +1132,7 @@ const ( DynaprepaidActionplansCfg = "dynaprepaid_actionprofile" MetaDynamicThreshold = "*dynamic_threshold" MetaDynamicStats = "*dynamic_stats" + MetaDynamicAttribute = "*dynamic_attribute" ) // Migrator Metas