The docs describe AttributeS as a Key-Value-Store, but that’s probably selling it short – You can do some really cool stuff with AttributeS, and in this post, we’re going to learn about using AttributeS to transform stuff.
Note: Before we get started, I’d suggest copying this config file to use for testing.
Let’s look at a really basic example, where we add some data into AttributeS, match based on Account in CGrateS, and get back that data.
Let’s look at how this would look on the API:
{ "method": "APIerSv2.SetAttributeProfile", "params": [{ "Tenant": "cgrates.org", "ID": "ATTR_Nick_Key_Value_Example", "Contexts": ["*any"], "FilterIDs": [ "*string:~*req.Account:1234" ], "Attributes": [ { "FilterIDs": [], "Path": "*req.ExampleKey", "Type": "*constant", "Value": "ExampleValue" } ], "Blocker": False, "Weight": 10 }], }
So what are we doing in this API call?
Well, for starters we’re calling the SetAttributeProfile endpoint, this is where we go to create / update Attribute Profiles, but in this case, because we’re hitting it for the first time with this ID, we’re creating a new entry called “ATTR_Nick_Key_Value_Example“, this will match any Contexts (more on them later) where the FilterIDs is a string, where the request Account, is equal to 1234.
Let’s run this against the CGrateS API and take a look at the result:
import cgrateshttpapi
import pprint
CGRateS_Obj = cgrateshttpapi.CGRateS('localhost', 2080)
SetAttributeProfile = {
"method": "APIerSv2.SetAttributeProfile",
"params": [{
"Tenant": "cgrates.org",
"ID": "ATTR_Nick_Key_Value_Example",
"Contexts": ["*any"],
"FilterIDs": [
"*string:~*req.Account:1234"
],
"Attributes": [
{
"FilterIDs": [],
"Path": "*req.ExampleKey",
"Type": "*constant",
"Value": "ExampleValue"
}
],
"Blocker": False,
"Weight": 10
}],
}
result = CGRateS_Obj.SendData(SetAttributeProfile)
pprint.pprint(result)
result = CGRateS_Obj.SendData({"method":"AttributeSv1.ProcessEvent",
"params":[
{"Tenant":"cgrates.org",
"Event":{"Account":"1234"},"APIOpts":{}}]})
pprint.pprint(result)
All going well you should have got the following back:
{'method': 'AttributeSv1.ProcessEvent', 'params': [{'Tenant': 'cgrates.org', 'Event': {'Account': '1234'}, 'APIOpts': {}}]} {'error': None, 'id': None, 'result': {'APIOpts': {}, 'AlteredFields': ['*req.ExampleKey'], 'Event': {'Account': '1234', 'ExampleKey': 'ExampleValue'}, 'ID': '', 'MatchedProfiles': ['cgrates.org:ATTR_Nick_Key_Value_Example'], 'Tenant': 'cgrates.org', 'Time': None}}
This tells us we matched the Attribute with the ID ATTR_Nick_Key_Value_Example, and inside Event we can see that ExampleKey was added with value ExampleValue.
Okay, you’re saying, well what was the point of that?
Well, what if as a key in the attributes, we had the password for the SIP account, which we passed to our SIP switch (Kamailio, FreeSWITCH or Asterisk for example), and used that to authenticate?
Let’s see how that would look:
{ "method": "APIerSv2.SetAttributeProfile", "params": [{ "Tenant": "cgrates.org", "ID": "ATTR_Nick_Password_Example", "Contexts": ["*any"], "FilterIDs": [ "*string:~*req.Account:1234" ], "Attributes": [ { "FilterIDs": [], "Path": "*req.SIP_password", "Type": "*constant", "Value": "sosecretiputitonthewebsite" } ], "Blocker": False, "Weight": 10 }], }
Now if the CGrateS Agent for your SIP Switch, includes the *attributes flag, and the call is coming from 1234, we’ll get back a key called “SIP_password” with the value “sosecretiputitonthewebsite”, which you can use to auth the SIP account.
We can also return multiple AttributeS, for example, we created two Attributes (ATTR_Nick_Password_Example and ATTR_Nick_Key_Value_Example) which match on the account 1234. This means we’ll get back the SIP Password from ATTR_Nick_Password_Example and the key:value we set in ATTR_Nick_Key_Value_Example:
{'method': 'AttributeSv1.ProcessEvent', 'params': [{'Tenant': 'cgrates.org', 'Event': {'Account': '1234'}}]} {'error': None, 'id': None, 'result': {'APIOpts': {}, 'AlteredFields': ['*req.SIP_password', '*req.ExampleKey'], 'Event': {'Account': '1234', 'ExampleKey': 'ExampleValue', 'SIP_password': 'sosecretiputitonthewebsite'}, 'ID': '', 'MatchedProfiles': ['cgrates.org:ATTR_Nick_Password_Example', 'cgrates.org:ATTR_Nick_Key_Value_Example'], 'Tenant': 'cgrates.org', 'Time': None}}
The order can be controlled by the Weight flag in the attribute, and if you want to stop matching any other AttributeS rules after the current Attribute, you can set the Blocker=True flag when you create/update the Attribute.
Okay, I hear you saying, that’s all well and good, I can add arbitrary key/values to stuff. Here endeth the lesson right?
Well not quite, because we can add key/values, but we can also rewrite variables using AttributeS.
Let’s imagine we’ve got 3 phone numbers (DIDs) associated with an account inside CGrateS, for example’s sake let’s say we have 12340001, 12340002 and 12340003, and we want any calls from these numbers to be billed to a CGrateS account called “NickTest1234”.
Our SIP switch doesn’t need to know anything about “NickTest1234”, just the 3 DIDs it can use to call out from your SIP stack. But to do this, we’d need CGrateS to transform any events from these DIDs to replace the Account value inside CGrateS, with NickTest1234.
Let’s see how that would look:
{'method': 'APIerSv2.SetAttributeProfile', 'params': [{'Tenant': 'cgrates.org', 'ID': 'ATTR_Calling_NickTest1234_12340001', 'Contexts': ['*any'], 'FilterIDs': ['*string:~*req.Account:12340001'], 'Attributes': [{'Path': '*req.Account', 'Type': '*constant', 'Value': 'NickTest1234'}], 'Weight': 0}], 'id': 1} {'method': 'APIerSv2.SetAttributeProfile', 'params': [{'Tenant': 'cgrates.org', 'ID': 'ATTR_Calling_NickTest1234_12340002', 'Contexts': ['*any'], 'FilterIDs': ['*string:~*req.Account:12340002'], 'Attributes': [{'Path': '*req.Account', 'Type': '*constant', 'Value': 'NickTest1234'}], 'Weight': 0}], 'id': 2} {'method': 'APIerSv2.SetAttributeProfile', 'params': [{'Tenant': 'cgrates.org', 'ID': 'ATTR_Calling_NickTest1234_12340003', 'Contexts': ['*any'], 'FilterIDs': ['*string:~*req.Account:12340003'], 'Attributes': [{'Path': '*req.Account', 'Type': '*constant', 'Value': 'NickTest1234'}], 'Weight': 0}], 'id': 3}
In the example code to go with this I’ve put together a simple for loop to add these – You can find the code on Github (link at the bottom).
So with these defined, let’s try and rate something, we’ll add a default Charger, and add an SMS balance, before simulating an SMS where the account is set to 12340003:
#Define default Charger print(CGRateS_Obj.SendData({"method":"APIerSv1.SetChargerProfile","params":[{"Tenant":"cgrates.org","ID":"DEFAULT","FilterIDs":[],"AttributeIDs":["*none"],"Weight":0}]})) #Add an SMS Balance print(CGRateS_Obj.SendData({"method":"ApierV1.SetBalance","params":[{"Tenant":"cgrates.org","Account":"Nick_Test_123","BalanceType":"*sms","Categories":"*any","Balance":{"ID":"SMS_Balance_1","Value":"100","Weight":25}}],"id":13})) import uuid import datetime now = datetime.datetime.now() result = CGRateS_Obj.SendData({ "method": "CDRsV2.ProcessExternalCDR", "params": [ { "OriginID": str(uuid.uuid1()), "ToR": "*sms", "RequestType": "*pseudoprepaid", "AnswerTime": now.strftime("%Y-%m-%d %H:%M:%S"), "SetupTime": now.strftime("%Y-%m-%d %H:%M:%S"), "Tenant": "cgrates.org", #This is going to be transformed to Nick_Test_123 by Attributes "Account": "12340003", "Usage": "1", } ] }) pprint.pprint(result)
Right, so all going well, here’s what you should see in the CDRs table:
Bing, despite the fact the Account in the ProcessExternalCDR was set to 12340003, and had no mention of “NickTest1234”, CGrateS transformed it to NickTest1234.
How did that happen? Well, inside our cgrates.json file we have set the cdrs and chargers modules to have a link to Attributes, which means that when we call CDRs or Chargers modules via the API, these will in turn bounce the data through AttributesS for any transformations.
This means we don’t need to run AttributeSv1.ProcessEvent ourselves, when we call CDRsV2.ProcessExternalCDR, the CDRs module will call AttributeSv1.ProcessEvent for us.
We can actually see this happening, using ngrep, which as you work more with CGrateS, is a tool you’ll get very familiar with, let’s take a peek:
sudo ngrep -t -W byline port 2012 -d lo
Now if we run the CDRsV2.ProcessExternalCDR again, we’ll see the CDRs module has called Attributes for us:
Boom, there it is, same as we ran, but it’s being handled by CGrateS for us.
If you look carefully you’ll see the context in the API request is set to “*cdrs”, this means the CDRs module is calling Attributes.
When we define each of our Attributes, as we did earlier in the post, we can set what contexts they are valid in, for example we may want to apply the transformation when called by CDRs, but not other modules, you can restrict that when you define the Attribute by setting “Contexts”: [“*cdrs”].
Okay, so we’ve done some account replacement, what else can we do?
Well, let’s look at some other use cases,
Here in Australia we’ve got a few valid dialing formats, you could dial E.164 format (Numbers look like: +61212341234), 0NSN format (Numbers look like: 02 1234 1234) or NSN format (Numbers look like: 1234 1234 assuming you’re in the 03 area code yourself).
If we want to define all our Destinations in E.164 format, we’ll need to to normalise the format using AttributeS, so the numbers always come as E.164.
Let’s give it a whirl with a static translation:
{ "method": "APIerSv2.SetAttributeProfile", "params": [{ "Tenant": "cgrates.org", "ID": "ATTR_0NSN_to_E164_Single", "Contexts": ["*any"], "FilterIDs": [ "*string:~*req.Subject:0212341234" ], "Attributes": [ { "FilterIDs": [], "Path": "*req.Subject", "Type": "*constant", "Value": "61212341234" } ], "Blocker": False, "Weight": 10 }], }
Now this will work, if we simulate an Event to AttributeS with the Subject 0212341234, it’ll get transformed by AttributeS to 61212341234.
The issue here is probably pretty obvious, the only matches one number, if we dial 0212341235 this all falls apart.
Enter our old friend Regex.
For starters, we’ll change the FilterIDs to match where the Account is NickTest7, this way we can set the rules on a per CGrateS account basis.
{ "method": "APIerSv2.SetAttributeProfile", "params": [{ "Tenant": "cgrates.org", "ID": "ATTR_0NSN_to_E164_02_Area_Code", "Contexts": ["*any"], "FilterIDs": [ "*string:~*req.Account:NickTest7" ], "Attributes": [ { "FilterIDs": [], "Path": "*req.Subject", "Type": "*variable", "Value": "~*req.Subject:s/^0(\d{1})(\d{8})$/61${1}${2}/" }, { "FilterIDs": [], "Path": "*req.Subject", "Type": "*variable", "Value": "~*req.Subject:s/^(\d{8})$/612${1}/" }, ], "Blocker": False, "Weight": 10 }], }
And then under AttributeS we’ve defined a rule to replace anything matching the 0NSN regex, to strip the first digit and append a 61, to put it in E.164 format, and in SN format as the second entry.
We can now test this out:
{'method': 'AttributeSv1.ProcessEvent', 'params': [{'Tenant': 'cgrates.org', 'Event': {'Account': 'NickTest7', 'Subject': '0312341234'}, 'APIOpts': {'*processRuns': 5, '*profileRuns': 5, '*subsys': '*sessions'}}]} {'error': None, 'id': None, 'result': {'APIOpts': {'*processRuns': 5, '*profileRuns': 5, '*subsys': '*sessions'}, 'AlteredFields': ['*req.Subject'], 'Event': {'Account': 'NickTest7', 'Subject': '61312341234'}, 'ID': '', 'MatchedProfiles': ['cgrates.org:ATTR_0NSN_to_E164_02_Area_Code'], 'Tenant': 'cgrates.org', 'Time': None}} {'method': 'AttributeSv1.ProcessEvent', 'params': [{'Tenant': 'cgrates.org', 'Event': {'Account': 'NickTest7', 'Subject': '12341234'}, 'APIOpts': {'*processRuns': 5, '*profileRuns': 5, '*subsys': '*sessions'}}]} {'error': None, 'id': None, 'result': {'APIOpts': {'*processRuns': 5, '*profileRuns': 5, '*subsys': '*sessions'}, 'AlteredFields': ['*req.Subject'], 'Event': {'Account': 'NickTest7', 'Subject': '61212341234'}, 'ID': '', 'MatchedProfiles': ['cgrates.org:ATTR_0NSN_to_E164_02_Area_Code'], 'Tenant': 'cgrates.org', 'Time': None}}
And there you have it folks; our number format standardized.
We can combo / cascade AttributeS rules together, with the aid of the Weight and Blocker flags in the API.
Let’s imagine the 61212341234 number has been ported from Operator1 to Operator2, and the Destinations we’ve defined in CGrateS for this prefix are currently set to DST_Operator1.
But because this number has been ported we should use DST_Operator2, so we charge the Operator2, as this number has been ported.
This means we don’t need to duplicate destination definitions to show this number has been ported, as this will be updated as the call gets rated, so we just assign the Attribute to each ported number.
So let’s match where the Subject of the call is 61212341234 (even though we’re going to input the Subject as 12341234), and rewrite the Destination attribute to DST_Operator2:
{ "method": "APIerSv2.SetAttributeProfile", "params": [{ "Tenant": "cgrates.org", "ID": "ATTR_Ported_61212341234", "Contexts": ["*any"], "FilterIDs": [ "*string:~*req.Subject:61212341234", ], "Attributes": [ { "FilterIDs": [], "Path": "*req.Destination", "Type": "*constant", "Value": "DST_Operator2" }, ], "Blocker": False, "Weight": 5 }], }
From the results we can see we matched two AttributeS rules, the first, ATTR_0NSN_to_E164_02_Area_Code reformatted the Subject of the call from 12341234 to 61212341234, then the updated Subject was passed through to ATTR_Ported_61212341234, which updated the Destination attribute to DST_Operator2.
{'method': 'AttributeSv1.ProcessEvent', 'params': [{'Tenant': 'cgrates.org', 'Event': {'Account': 'NickTest7', 'Subject': '12341234'}, 'APIOpts': {'*processRuns': 5, '*profileRuns': 5, '*subsys': '*sessions'}}]} {'error': None, 'id': None, 'result': {'APIOpts': {'*processRuns': 5, '*profileRuns': 5, '*subsys': '*sessions'}, 'AlteredFields': ['*req.Subject', '*req.Destination'], 'Event': {'Account': 'NickTest7', 'Destination': 'DST_Operator2', 'Subject': '61212341234'}, 'ID': '', 'MatchedProfiles': ['cgrates.org:ATTR_0NSN_to_E164_02_Area_Code', 'cgrates.org:ATTR_Ported_61212341234'], 'Tenant': 'cgrates.org', 'Time': None}}
Hopefully this has helped you to dip a toe into the CGrateS AttributeS pool, and give you some ideas of what we can achieve inside AttributeS.
A complete working code & config is available on my Github here.
If you’re having issues, make sure you have loaded the config file, are running the latest version, and if in doubt (and not on a production system), this script will clear all the data for you so you can rule out anything interfering.