From 6ec0cfb4bc0c4d8a207e92262dbcba9e327198bd Mon Sep 17 00:00:00 2001 From: ionutboangiu Date: Wed, 4 Dec 2024 13:36:10 +0200 Subject: [PATCH] Allow configurable event field updates in ARI sessions --- agents/astagent.go | 10 ++++++++-- agents/asterisk_event.go | 28 +++++++++++++++++++++++++++- config/config_defaults.go | 16 ++++++++++------ config/libconfig_json.go | 9 +++++---- config/smconfig.go | 22 ++++++++++++++-------- config/smconfig_test.go | 30 ++++++++++++++++-------------- 6 files changed, 80 insertions(+), 35 deletions(-) diff --git a/agents/astagent.go b/agents/astagent.go index e8630ed4d..4a7762e32 100644 --- a/agents/astagent.go +++ b/agents/astagent.go @@ -249,8 +249,10 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { if !hasIt { // Not handled by us return } + + // Update the cached event with new channel state. sma.evCacheMux.Lock() - err := ev.UpdateCGREvent(cgrEvDisp.CGREvent) // Updates the event directly in the cache + err := ev.UpdateCGREvent(cgrEvDisp.CGREvent, sma.cgrCfg.AsteriskAgentCfg().AlterableFields) sma.evCacheMux.Unlock() if err != nil { sma.hangupChannel(ev.ChannelID(), @@ -258,6 +260,7 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { utils.AsteriskAgent, err.Error(), ev.ChannelID())) return } + // populate init session args initSessionArgs := ev.V1InitSessionArgs(*cgrEvDisp) if initSessionArgs == nil { @@ -289,8 +292,10 @@ func (sma *AsteriskAgent) handleChannelDestroyed(ev *SMAsteriskEvent) { if !hasIt { // Not handled by us return } + + // Update the cached event with new channel state. sma.evCacheMux.Lock() - err := ev.UpdateCGREvent(cgrEvDisp.CGREvent) // Updates the event directly in the cache + err := ev.UpdateCGREvent(cgrEvDisp.CGREvent, sma.cgrCfg.AsteriskAgentCfg().AlterableFields) sma.evCacheMux.Unlock() if err != nil { utils.Logger.Warning( @@ -298,6 +303,7 @@ func (sma *AsteriskAgent) handleChannelDestroyed(ev *SMAsteriskEvent) { utils.AsteriskAgent, err.Error(), ev.ChannelID())) return } + // populate terminate session args tsArgs := ev.V1TerminateSessionArgs(*cgrEvDisp) if tsArgs == nil { diff --git a/agents/asterisk_event.go b/agents/asterisk_event.go index 8c7d3d697..cf3336e0d 100644 --- a/agents/asterisk_event.go +++ b/agents/asterisk_event.go @@ -19,6 +19,7 @@ along with this program. If not, see package agents import ( + "errors" "fmt" "strings" @@ -186,7 +187,8 @@ func (smaEv *SMAsteriskEvent) ExtraParameters() (extraParams map[string]string) return } -func (smaEv *SMAsteriskEvent) UpdateCGREvent(cgrEv *utils.CGREvent) error { +// UpdateCGREvent updates a previously cached CGREvent (from StasisStart) with data from a new ARI event. +func (smaEv *SMAsteriskEvent) UpdateCGREvent(cgrEv *utils.CGREvent, alterableFields []string) error { resCGREv := *cgrEv switch smaEv.EventType() { case ARIChannelStateChange: @@ -212,6 +214,30 @@ func (smaEv *SMAsteriskEvent) UpdateCGREvent(cgrEv *utils.CGREvent) error { } } } + + // Update fields specified in alterableFields. Each field can be in the format: + // - "path.to.field": copies the field value using the last path element as key + // - "path.to.field:alias": copies the field value using the alias as key + if len(alterableFields) != 0 { + ms := utils.MapStorage(smaEv.ariEv) + for _, field := range alterableFields { + path, alias, hasAlias := strings.Cut(field, utils.InInFieldSep) + pathElems := strings.Split(path, utils.NestingSep) + fieldVal, err := ms.FieldAsString(pathElems) + if errors.Is(err, utils.ErrNotFound) { + continue + } + if err != nil { + return err + } + fieldKey := pathElems[len(pathElems)-1] + if hasAlias { + fieldKey = alias + } + resCGREv.Event[fieldKey] = fieldVal + } + } + *cgrEv = resCGREv return nil } diff --git a/config/config_defaults.go b/config/config_defaults.go index 8aec081fb..be92860bb 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -369,15 +369,19 @@ const CGRATES_CFG_JSON = ` "asterisk_agent": { - "enabled": false, // starts the Asterisk agent: + "enabled": false, // starts the Asterisk agent: "sessions_conns": ["*internal"], - "create_cdr": false, // create CDR out of events and sends it to CDRS component - "asterisk_conns":[ // instantiate connections to multiple Asterisk servers - {"address": "127.0.0.1:8088", "user": "cgrates", "password": "CGRateS.org", "connect_attempts": 3,"reconnects": 5} - ], + "create_cdr": false, // create CDR out of events and sends it to CDRS component + //"alterable_fields": [], // additional fields to update from ARI events, optionally with :alias + "asterisk_conns": [{ // instantiate connections to multiple Asterisk servers + "address": "127.0.0.1:8088", + "user": "cgrates", + "password": "CGRateS.org", + "connect_attempts": 3, + "reconnects": 5 + }] }, - "freeswitch_agent": { "enabled": false, // starts the FreeSWITCH agent: "sessions_conns": ["*internal"], diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 1c0ec0ed6..1b8e1dedf 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -268,10 +268,11 @@ type AstConnJsonCfg struct { } type AsteriskAgentJsonCfg struct { - Enabled *bool - Sessions_conns *[]string - Create_cdr *bool - Asterisk_conns *[]*AstConnJsonCfg + Enabled *bool + Sessions_conns *[]string + Create_cdr *bool + Alterable_fields *[]string + Asterisk_conns *[]*AstConnJsonCfg } type CacheParamJsonCfg struct { diff --git a/config/smconfig.go b/config/smconfig.go index b07295b80..5de32c7b5 100644 --- a/config/smconfig.go +++ b/config/smconfig.go @@ -20,6 +20,7 @@ package config import ( "fmt" + "slices" "strconv" "strings" "time" @@ -598,10 +599,11 @@ func (aConnCfg *AsteriskConnCfg) AsMapInterface() map[string]any { } type AsteriskAgentCfg struct { - Enabled bool - SessionSConns []string - CreateCDR bool - AsteriskConns []*AsteriskConnCfg + Enabled bool + SessionSConns []string + CreateCDR bool + AlterableFields []string + AsteriskConns []*AsteriskConnCfg } func (aCfg *AsteriskAgentCfg) loadFromJsonCfg(jsnCfg *AsteriskAgentJsonCfg) (err error) { @@ -625,6 +627,9 @@ func (aCfg *AsteriskAgentCfg) loadFromJsonCfg(jsnCfg *AsteriskAgentJsonCfg) (err if jsnCfg.Create_cdr != nil { aCfg.CreateCDR = *jsnCfg.Create_cdr } + if jsnCfg.Alterable_fields != nil { + aCfg.AlterableFields = *jsnCfg.Alterable_fields + } if jsnCfg.Asterisk_conns != nil { aCfg.AsteriskConns = make([]*AsteriskConnCfg, len(*jsnCfg.Asterisk_conns)) for i, jsnAConn := range *jsnCfg.Asterisk_conns { @@ -652,9 +657,10 @@ func (aCfg *AsteriskAgentCfg) AsMapInterface() map[string]any { } return map[string]any{ - utils.EnabledCfg: aCfg.Enabled, - utils.SessionSConnsCfg: sessionSConns, - utils.CreateCDRCfg: aCfg.CreateCDR, - utils.AsteriskConnsCfg: conns, + utils.EnabledCfg: aCfg.Enabled, + utils.SessionSConnsCfg: sessionSConns, + utils.CreateCDRCfg: aCfg.CreateCDR, + utils.AlterableFieldsCfg: slices.Clone(aCfg.AlterableFields), + utils.AsteriskConnsCfg: conns, } } diff --git a/config/smconfig_test.go b/config/smconfig_test.go index 844e0f0ae..1b635a176 100644 --- a/config/smconfig_test.go +++ b/config/smconfig_test.go @@ -497,15 +497,17 @@ func TestAsteriskAgentCfgAsMapInterface(t *testing.T) { "enabled": true, "sessions_conns": ["*internal"], "create_cdr": false, + "alterable_fields": ["field1", "field2"], "asterisk_conns":[ {"address": "127.0.0.1:8088", "user": "cgrates", "password": "CGRateS.org", "connect_attempts": 3,"reconnects": 5} ], }, }` eMap := map[string]any{ - "enabled": true, - "sessions_conns": []string{"*internal"}, - "create_cdr": false, + "enabled": true, + "sessions_conns": []string{"*internal"}, + "create_cdr": false, + utils.AlterableFieldsCfg: []string{"field1", "field2"}, "asterisk_conns": []map[string]any{ {"alias": "", "address": "127.0.0.1:8088", "user": "cgrates", "password": "CGRateS.org", "connect_attempts": 3, "reconnects": 5}, }, @@ -517,7 +519,7 @@ func TestAsteriskAgentCfgAsMapInterface(t *testing.T) { } else if err = asagcfg.loadFromJsonCfg(jsnAsAgCfg); err != nil { t.Error(err) } else if rcv := asagcfg.AsMapInterface(); !reflect.DeepEqual(eMap, rcv) { - t.Errorf("\nExpected: %+v\nReceived: %+v", utils.ToJSON(eMap), utils.ToJSON(rcv)) + t.Errorf("expected: %s, received: %s", utils.ToJSON(eMap), utils.ToJSON(rcv)) } } @@ -912,9 +914,10 @@ func TestSMConfigAsteriskAgentCfgloadFromJsonCfg(t *testing.T) { func TestSMConfigAsteriskAgentCfgAsMapInterface(t *testing.T) { str := "test" aCfg := &AsteriskAgentCfg{ - Enabled: false, - SessionSConns: []string{str}, - CreateCDR: false, + Enabled: false, + SessionSConns: []string{str}, + CreateCDR: false, + AlterableFields: []string{"field1", "field2"}, AsteriskConns: []*AsteriskConnCfg{ { Alias: str, @@ -927,15 +930,14 @@ func TestSMConfigAsteriskAgentCfgAsMapInterface(t *testing.T) { }, } exp := map[string]any{ - utils.EnabledCfg: aCfg.Enabled, - utils.SessionSConnsCfg: []string{str}, - utils.CreateCDRCfg: aCfg.CreateCDR, - utils.AsteriskConnsCfg: []map[string]any{aCfg.AsteriskConns[0].AsMapInterface()}, + utils.EnabledCfg: aCfg.Enabled, + utils.SessionSConnsCfg: []string{str}, + utils.CreateCDRCfg: aCfg.CreateCDR, + utils.AlterableFieldsCfg: []string{"field1", "field2"}, + utils.AsteriskConnsCfg: []map[string]any{aCfg.AsteriskConns[0].AsMapInterface()}, } - rcv := aCfg.AsMapInterface() - - if !reflect.DeepEqual(exp, rcv) { + if rcv := aCfg.AsMapInterface(); !reflect.DeepEqual(exp, rcv) { t.Errorf("expected %s, received %s", utils.ToJSON(exp), utils.ToJSON(rcv)) } }