From 064fb72f3cd39bbac3ddb839ddd8fcaa51dc70eb Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 12 Nov 2018 16:10:05 +0200 Subject: [PATCH 01/12] Fixes for memory profiling --- cmd/cgr-engine/cgr-engine.go | 67 +++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index 82fea2dbb..c47b51ff9 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -62,8 +62,12 @@ var ( cfgDir = flag.String("config_dir", utils.CONFIG_DIR, "Configuration directory path.") version = flag.Bool("version", false, "Prints the application version.") pidFile = flag.String("pid", "", "Write pid file") - cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") - memprofile = flag.String("memprofile", "", "write memory profile to file") + cpupath = flag.String("cpupath", "", "write cpu profile to files") + cputimeout = flag.Duration("cputimeout", 5*time.Second, "Time betwen cpu profile saves") + cpuNoFiles = flag.Int("cpuNoFiles", 5, "Number of cpu profile to write") + mempath = flag.String("mempath", "", "write memory profile to file") + memtimeout = flag.Duration("memtimeout", 5*time.Second, "Time betwen memory profile saves") + memNoFiles = flag.Int("memNoFiles", 5, "Number of memory profile to write") scheduledShutdown = flag.String("scheduled_shutdown", "", "shutdown the engine after this duration") singlecpu = flag.Bool("singlecpu", false, "Run on single CPU core") syslogger = flag.String("logger", "", "logger <*syslog|*stdout>") @@ -1291,7 +1295,38 @@ func schedCDRsConns(internalCDRSChan chan rpcclient.RpcClientConnection, exitCha } engine.SetSchedCdrsConns(cdrsConn) } - +func memprofiling(mempath string, timeout time.Duration, noFile int) { + for i := 1; ; i++ { + time.Sleep(timeout) + f, err := os.Create(fmt.Sprintf("%smem%v.prof", mempath, i)) + if err != nil { + log.Fatal("could not create memory profile file: ", err) + } + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Fatal("could not write memory profile: ", err) + } + f.Close() + if i%noFile == 0 { + i = 0 // reset the counting + } + } +} +func cpuprofiling(cpupath string, timeout time.Duration, noFile int) { + for i := 1; ; i++ { + f, err := os.Create(fmt.Sprintf("%scpu%v.prof", cpupath, i)) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + time.Sleep(timeout) //wait to profile + pprof.StopCPUProfile() + f.Close() + if i%noFile == 0 { + i = 0 // reset the counting + } + } +} func main() { flag.Parse() if *version { @@ -1305,14 +1340,12 @@ func main() { runtime.GOMAXPROCS(1) // Having multiple cpus may slow down computing due to CPU management, to be reviewed in future Go releases } exitChan := make(chan bool) - if *cpuprofile != "" { - f, err := os.Create(*cpuprofile) - if err != nil { - log.Fatal(err) - } - defer f.Close() - pprof.StartCPUProfile(f) - defer pprof.StopCPUProfile() + + if *mempath != "" { + go memprofiling(*mempath, *memtimeout, *memNoFiles) + } + if *cpupath != "" { + go cpuprofiling(*cpupath, *cputimeout, *cpuNoFiles) } if *scheduledShutdown != "" { shutdownDur, err := utils.ParseDurationWithNanosecs(*scheduledShutdown) @@ -1574,18 +1607,6 @@ func main() { internalSMGChan, internalDispatcherSChan, internalAnalyzerSChan, exitChan) <-exitChan - if *memprofile != "" { - f, err := os.Create(*memprofile) - if err != nil { - log.Fatal("could not create memory profile file: ", err) - } - defer f.Close() - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Fatal("could not write memory profile: ", err) - } - } - if *pidFile != "" { if err := os.Remove(*pidFile); err != nil { utils.Logger.Warning("Could not remove pid file: " + err.Error()) From 063c29400eed9a45c391274bb3fd626f2849fe86 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 13 Nov 2018 11:50:58 +0200 Subject: [PATCH 02/12] Fixes for cpu and memory profiling --- cmd/cgr-engine/cgr-engine.go | 108 +++++++++++++++++++++------------ cmd/cgr-engine/registration.go | 57 ----------------- 2 files changed, 70 insertions(+), 95 deletions(-) delete mode 100644 cmd/cgr-engine/registration.go diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index c47b51ff9..c6ba5cebc 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -23,10 +23,13 @@ import ( "fmt" "log" "os" + "os/signal" + "path" "runtime" "runtime/pprof" "strconv" "strings" + "syscall" "time" "github.com/cgrates/cgrates/agents" @@ -62,12 +65,10 @@ var ( cfgDir = flag.String("config_dir", utils.CONFIG_DIR, "Configuration directory path.") version = flag.Bool("version", false, "Prints the application version.") pidFile = flag.String("pid", "", "Write pid file") - cpupath = flag.String("cpupath", "", "write cpu profile to files") - cputimeout = flag.Duration("cputimeout", 5*time.Second, "Time betwen cpu profile saves") - cpuNoFiles = flag.Int("cpuNoFiles", 5, "Number of cpu profile to write") - mempath = flag.String("mempath", "", "write memory profile to file") - memtimeout = flag.Duration("memtimeout", 5*time.Second, "Time betwen memory profile saves") - memNoFiles = flag.Int("memNoFiles", 5, "Number of memory profile to write") + cpuProfDir = flag.String("cpuprof_dir", "", "write cpu profile to files") + memProfDir = flag.String("memprof_dir", "", "write memory profile to file") + memProfInterval = flag.Duration("memprof_interval", 5*time.Second, "Time betwen memory profile saves") + memProfNrFiles = flag.Int("memprof_nrfiles", 1, "Number of memory profile to write") scheduledShutdown = flag.String("scheduled_shutdown", "", "shutdown the engine after this duration") singlecpu = flag.Bool("singlecpu", false, "Run on single CPU core") syslogger = flag.String("logger", "", "logger <*syslog|*stdout>") @@ -1295,38 +1296,58 @@ func schedCDRsConns(internalCDRSChan chan rpcclient.RpcClientConnection, exitCha } engine.SetSchedCdrsConns(cdrsConn) } -func memprofiling(mempath string, timeout time.Duration, noFile int) { - for i := 1; ; i++ { - time.Sleep(timeout) - f, err := os.Create(fmt.Sprintf("%smem%v.prof", mempath, i)) - if err != nil { - log.Fatal("could not create memory profile file: ", err) - } - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Fatal("could not write memory profile: ", err) - } + +func memProfFile(memProfPath string) bool { + f, err := os.Create(memProfPath) + if err != nil { + utils.Logger.Crit(fmt.Sprintf("could not create memory profile file: %s", err)) + return false + } + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + utils.Logger.Crit(fmt.Sprintf("could not write memory profile: %s", err)) f.Close() - if i%noFile == 0 { + return false + } + f.Close() + return true +} + +func memProfiling(memProfDir string, interval time.Duration, nrFiles int, exitChan chan bool) { + for i := 1; ; i++ { + time.Sleep(interval) + memPath := path.Join(memProfDir, fmt.Sprintf("mem%v.prof", i)) + if !memProfFile(memPath) { + exitChan <- true + } + if i%nrFiles == 0 { i = 0 // reset the counting } } } -func cpuprofiling(cpupath string, timeout time.Duration, noFile int) { - for i := 1; ; i++ { - f, err := os.Create(fmt.Sprintf("%scpu%v.prof", cpupath, i)) - if err != nil { - log.Fatal(err) - } - pprof.StartCPUProfile(f) - time.Sleep(timeout) //wait to profile - pprof.StopCPUProfile() - f.Close() - if i%noFile == 0 { - i = 0 // reset the counting - } + +func cpuProfiling(cpuProfDir string, exitChan chan bool, stopChan, doneChan chan struct{}) { + cpuPath := path.Join(cpuProfDir, "cpu.prof") + f, err := os.Create(cpuPath) + if err != nil { + utils.Logger.Crit(fmt.Sprintf("could not create cpu profile file: %s", err)) + exitChan <- true + return } + pprof.StartCPUProfile(f) + <-stopChan + pprof.StopCPUProfile() + f.Close() + doneChan <- struct{}{} } + +func shutdownSingnalHandler(exitChan chan bool) { + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) + <-c + exitChan <- true +} + func main() { flag.Parse() if *version { @@ -1339,14 +1360,19 @@ func main() { if *singlecpu { runtime.GOMAXPROCS(1) // Having multiple cpus may slow down computing due to CPU management, to be reviewed in future Go releases } - exitChan := make(chan bool) - if *mempath != "" { - go memprofiling(*mempath, *memtimeout, *memNoFiles) + exitChan := make(chan bool) + go shutdownSingnalHandler(exitChan) + + if *memProfDir != "" { + go memProfiling(*memProfDir, *memProfInterval, *memProfNrFiles, exitChan) } - if *cpupath != "" { - go cpuprofiling(*cpupath, *cputimeout, *cpuNoFiles) + cpuProfChanStop := make(chan struct{}) + cpuProfChanDone := make(chan struct{}) + if *cpuProfDir != "" { + go cpuProfiling(*cpuProfDir, exitChan, cpuProfChanStop, cpuProfChanDone) } + if *scheduledShutdown != "" { shutdownDur, err := utils.ParseDurationWithNanosecs(*scheduledShutdown) if err != nil { @@ -1380,6 +1406,7 @@ func main() { lgLevel = *logLevel } utils.Logger.SetLogLevel(lgLevel) + var loadDb engine.LoadStorage var cdrDb engine.CdrStorage var dm *engine.DataManager @@ -1514,8 +1541,6 @@ func main() { // Start FreeSWITCHAgent if cfg.FsAgentCfg().Enabled { go startFsAgent(internalSMGChan, exitChan) - // close all sessions on shutdown - go shutdownSessionmanagerSingnalHandler(exitChan) } // Start SM-Kamailio @@ -1607,6 +1632,13 @@ func main() { internalSMGChan, internalDispatcherSChan, internalAnalyzerSChan, exitChan) <-exitChan + if *cpuProfDir != "" { // wait to end cpuProfiling + cpuProfChanStop <- struct{}{} + <-cpuProfChanDone + } + if *memProfDir != "" { // write last memory profiling + memProfFile(path.Join(*memProfDir, "mem_final.prof")) + } if *pidFile != "" { if err := os.Remove(*pidFile); err != nil { utils.Logger.Warning("Could not remove pid file: " + err.Error()) diff --git a/cmd/cgr-engine/registration.go b/cmd/cgr-engine/registration.go deleted file mode 100644 index a65348da0..000000000 --- a/cmd/cgr-engine/registration.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments -Copyright (C) ITsysCOM GmbH - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see -*/ - -package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/cgrates/cgrates/utils" - "github.com/cgrates/rpcclient" -) - -/* -Listens for the SIGTERM, SIGINT, SIGQUIT system signals and closes the storage before exiting. -*/ -func stopRaterSignalHandler(internalCdrStatSChan chan rpcclient.RpcClientConnection, exitChan chan bool) { - c := make(chan os.Signal) - signal.Notify(c, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) - sig := <-c - - utils.Logger.Info(fmt.Sprintf("Caught signal %v", sig)) - var dummyInt int - select { - case cdrStats := <-internalCdrStatSChan: - cdrStats.Call("CDRStatsV1.Stop", dummyInt, &dummyInt) - default: - } - exitChan <- true -} - -/* -Listens for the SIGTERM, SIGINT, SIGQUIT system signals and shuts down the session manager. -*/ -func shutdownSessionmanagerSingnalHandler(exitChan chan bool) { - c := make(chan os.Signal) - signal.Notify(c, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) - <-c - exitChan <- true -} From ead9bb2e27c88f5b9fcb4cbedb82214344acacd5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 15 Nov 2018 16:49:32 +0200 Subject: [PATCH 03/12] Fixes and tests for #1304 --- apier/v1/apier_it_test.go | 2 +- apier/v1/cdre_it_test.go | 90 +++++++++++++++++++++++++++++++++++++++ apier/v1/stats_it_test.go | 6 ++- engine/cdr.go | 1 + engine/cdrs.go | 3 ++ 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/apier/v1/apier_it_test.go b/apier/v1/apier_it_test.go index c0bbf1141..ab7144d63 100644 --- a/apier/v1/apier_it_test.go +++ b/apier/v1/apier_it_test.go @@ -698,7 +698,7 @@ func TestApierLoadAccountActions(t *testing.T) { if err := rater.Call("ApierV1.GetCacheStats", args, &rcvStats); err != nil { t.Error("Got error on ApierV1.GetCacheStats: ", err.Error()) } else if !reflect.DeepEqual(expectedStats, rcvStats) { - t.Errorf("Calling ApierV1.GetCacheStats expected: %+v, received: %+v", expectedStats, rcvStats) + t.Errorf("Calling ApierV1.GetCacheStats expected: %+v, received: %+v", utils.ToJSON(expectedStats), utils.ToJSON(rcvStats)) } } diff --git a/apier/v1/cdre_it_test.go b/apier/v1/cdre_it_test.go index 4f0032cc4..fce76fdb4 100755 --- a/apier/v1/cdre_it_test.go +++ b/apier/v1/cdre_it_test.go @@ -24,6 +24,7 @@ import ( "net/rpc" "net/rpc/jsonrpc" "path" + "reflect" "testing" "time" @@ -49,6 +50,8 @@ var sTestsCDRE = []func(t *testing.T){ testCDReRPCConn, testCDReAddCDRs, testCDReExportCDRs, + testCDReFromFolder, + testCDReProcessExternalCdr, testCDReKillEngine, } @@ -156,6 +159,93 @@ func testCDReExportCDRs(t *testing.T) { } } +func testCDReFromFolder(t *testing.T) { + var reply string + attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "tutorial")} + if err := cdreRPC.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil { + t.Error(err) + } + time.Sleep(500 * time.Millisecond) +} + +// Test CDR from external sources +func testCDReProcessExternalCdr(t *testing.T) { + cdr := &engine.ExternalCDR{ToR: utils.VOICE, + OriginID: "testextcdr1", OriginHost: "127.0.0.1", Source: utils.UNIT_TEST, RequestType: utils.META_RATED, + Tenant: "cgrates.org", Category: "call", Account: "1003", Subject: "1003", Destination: "1001", + SetupTime: "2014-08-04T13:00:00Z", AnswerTime: "2014-08-04T13:00:07Z", + Usage: "1s", ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, + } + var reply string + if err := cdreRPC.Call("CdrsV1.ProcessExternalCdr", cdr, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received: ", reply) + } + time.Sleep(50 * time.Millisecond) + var cdrs []*engine.ExternalCDR + args := utils.RPCCDRsFilter{OriginIDs: []string{"testextcdr1"}} + if err := cdreRPC.Call("ApierV2.GetCdrs", args, &cdrs); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(cdrs) != 2 { + t.Errorf("Unexpected number of CDRs returned: %v, cdrs=%s ", len(cdrs), utils.ToJSON(cdrs)) + return + } else { + for _, c := range cdrs { + if c.RunID == utils.MetaRaw && c.Cost != -1 { + t.Errorf("Expected for *raw cdr cost to be -1, recived: %v", c.Cost) + } + if c.RunID == utils.MetaDefault && c.Cost != 0.3 { + t.Errorf("Expected for *default cdr cost to be 0.3, recived: %v", c.Cost) + } + if c.RunID == utils.MetaDefault { + acdr, err := engine.NewCDRFromExternalCDR(c, "") + if err != nil { + t.Error(err) + return + } + if acdr.CostDetails == nil { + t.Errorf("CostDetails should not be nil") + return + } + if acdr.CostDetails.Usage == nil { + t.Errorf("CostDetails for procesed cdr has usage nil") + } + if acdr.CostDetails.Cost == nil { + t.Errorf("CostDetails for procesed cdr has cost nil") + } + } + if c.Usage != "1s" { + t.Errorf("Expected 1s,recived %s", c.Usage) + } + if c.Source != utils.UNIT_TEST { + t.Errorf("Expected %s,recived %s", utils.UNIT_TEST, c.Source) + } + if c.ToR != utils.VOICE { + t.Errorf("Expected %s,recived %s", utils.VOICE, c.ToR) + } + if c.RequestType != utils.META_RATED { + t.Errorf("Expected %s,recived %s", utils.META_RATED, c.RequestType) + } + if c.Category != "call" { + t.Errorf("Expected call,recived %s", c.Category) + } + if c.Account != "1003" { + t.Errorf("Expected 1003,recived %s", c.Account) + } + if c.Subject != "1003" { + t.Errorf("Expected 1003,recived %s", c.Subject) + } + if c.Destination != "1001" { + t.Errorf("Expected 1001,recived %s", c.Destination) + } + if !reflect.DeepEqual(c.ExtraFields, cdr.ExtraFields) { + t.Errorf("Expected %s,recived %s", utils.ToJSON(c.ExtraFields), utils.ToJSON(cdr.ExtraFields)) + } + } + } +} + func testCDReKillEngine(t *testing.T) { if err := engine.KillEngine(100); err != nil { t.Error(err) diff --git a/apier/v1/stats_it_test.go b/apier/v1/stats_it_test.go index c207ab68b..5b9676040 100644 --- a/apier/v1/stats_it_test.go +++ b/apier/v1/stats_it_test.go @@ -393,13 +393,12 @@ func testV1STSUpdateStatQueueProfile(t *testing.T) { } else if result != utils.OK { t.Error("Unexpected reply returned", result) } - time.Sleep(time.Second) var reply *engine.StatQueueProfile if err := stsV1Rpc.Call("ApierV1.GetStatQueueProfile", &utils.TenantID{Tenant: "cgrates.org", ID: "TEST_PROFILE1"}, &reply); err != nil { t.Error(err) } else if !reflect.DeepEqual(statConfig, reply) { - t.Errorf("Expecting: %+v, received: %+v", statConfig, reply) + t.Errorf("Expecting: %+v, received: %+v", utils.ToJSON(statConfig), utils.ToJSON(reply)) } } @@ -411,6 +410,9 @@ func testV1STSRemoveStatQueueProfile(t *testing.T) { } else if resp != utils.OK { t.Error("Unexpected reply returned", resp) } + if tSv1ConfDIR == "tutmongo" { + time.Sleep(150 * time.Millisecond) + } var sqp *engine.StatQueueProfile if err := stsV1Rpc.Call("ApierV1.GetStatQueueProfile", &utils.TenantID{Tenant: "cgrates.org", ID: "TEST_PROFILE1"}, &sqp); err == nil || err.Error() != utils.ErrNotFound.Error() { diff --git a/engine/cdr.go b/engine/cdr.go index 251c5fcb7..b8435625b 100644 --- a/engine/cdr.go +++ b/engine/cdr.go @@ -55,6 +55,7 @@ func NewCDRFromExternalCDR(extCdr *ExternalCDR, timezone string) (*CDR, error) { } } if len(extCdr.CostDetails) != 0 { + cdr.CostDetails = &EventCost{} if err = json.Unmarshal([]byte(extCdr.CostDetails), cdr.CostDetails); err != nil { return nil, err } diff --git a/engine/cdrs.go b/engine/cdrs.go index 66b5515ca..2cbbe8e42 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -283,6 +283,9 @@ func (self *CdrServer) deriveRateStoreStatsReplicate(cdr *CDR, store, cdrstats, // Store rated CDRs if store { for _, ratedCDR := range ratedCDRs { + if ratedCDR.CostDetails != nil { + ratedCDR.CostDetails.Compute() + } if err := self.cdrDb.SetCDR(ratedCDR, true); err != nil { utils.Logger.Err(fmt.Sprintf(" Storing rated CDR %+v, got error: %s", ratedCDR, err.Error())) } From ba5e0b7cd768ddea2463579eb03321def51e0335 Mon Sep 17 00:00:00 2001 From: TeoV Date: Tue, 13 Nov 2018 10:02:37 -0500 Subject: [PATCH 04/12] Update calls test for asterisk --- agents/asterisk_event.go | 103 ++++++------------ agents/asteriskagent.go | 35 +++++- agents/libhttpagent_test.go | 10 ++ cmd/cgr-engine/cgr-engine.go | 8 +- .../asterisk/etc/asterisk/extensions.conf | 2 +- .../asterisk/etc/asterisk/http.conf | 2 +- .../asterisk/etc/asterisk/pjsip.conf | 7 +- .../cgrates/etc/cgrates/cgrates.json | 39 ++++--- general_tests/tutorial_calls_test.go | 4 +- sessions/sessions.go | 2 + 10 files changed, 113 insertions(+), 99 deletions(-) diff --git a/agents/asterisk_event.go b/agents/asterisk_event.go index dd553db2a..fa52c14b8 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 ( + "fmt" "strings" "github.com/cgrates/cgrates/config" @@ -273,37 +274,17 @@ func (smaEv *SMAsteriskEvent) V1AuthorizeArgs() (args *sessions.V1AuthorizeArgs) GetMaxUsage: true, CGREvent: *cgrEv, } - // For the moment hardcoded only GetMaxUsage : true - /* - subsystems, has := kev[KamCGRSubsystems] - if !has { - return - } - if strings.Index(subsystems, utils.MetaAccounts) == -1 { - args.GetMaxUsage = false - } - if strings.Index(subsystems, utils.MetaResources) != -1 { - args.AuthorizeResources = true - } - if strings.Index(subsystems, utils.MetaSuppliers) != -1 { - args.GetSuppliers = true - if strings.Index(subsystems, utils.MetaSuppliersEventCost) != -1 { - args.SuppliersMaxCost = utils.MetaEventCost - } - if strings.Index(subsystems, utils.MetaSuppliersIgnoreErrors) != -1 { - args.SuppliersIgnoreErrors = true - } - } - if strings.Index(subsystems, utils.MetaAttributes) != -1 { - args.GetAttributes = true - } - if strings.Index(subsystems, utils.MetaThresholds) != -1 { - args.ProcessThresholds = utils.BoolPointer(true) - } - if strings.Index(subsystems, utils.MetaStats) != -1 { - args.ProcessStatQueues = utils.BoolPointer(true) - } - */ + + args.GetMaxUsage = strings.Index(smaEv.Subsystems(), utils.MetaAccounts) != -1 + args.AuthorizeResources = strings.Index(smaEv.Subsystems(), utils.MetaResources) != -1 + args.GetSuppliers = strings.Index(smaEv.Subsystems(), utils.MetaSuppliers) != -1 + args.SuppliersIgnoreErrors = strings.Index(smaEv.Subsystems(), utils.MetaSuppliersIgnoreErrors) != -1 + if strings.Index(smaEv.Subsystems(), utils.MetaSuppliersEventCost) != -1 { + args.SuppliersMaxCost = utils.MetaEventCost + } + args.GetAttributes = strings.Index(smaEv.Subsystems(), utils.MetaAttributes) != -1 + args.ProcessThresholds = strings.Index(smaEv.Subsystems(), utils.MetaThresholds) != -1 + args.ProcessStats = strings.Index(smaEv.Subsystems(), utils.MetaStats) != -1 return } @@ -312,27 +293,17 @@ func (smaEv *SMAsteriskEvent) V1InitSessionArgs(cgrEv utils.CGREvent) (args *ses InitSession: true, CGREvent: cgrEv, } - /* - subsystems, has := kev[KamCGRSubsystems] - if !has { - return - } - if strings.Index(subsystems, utils.MetaAccounts) == -1 { - args.InitSession = false - } - if strings.Index(subsystems, utils.MetaResources) != -1 { - args.AllocateResources = true - } - if strings.Index(subsystems, utils.MetaAttributes) != -1 { - args.GetAttributes = true - } - if strings.Index(subsystems, utils.MetaThresholds) != -1 { - args.ProcessThresholds = utils.BoolPointer(true) - } - if strings.Index(subsystems, utils.MetaStats) != -1 { - args.ProcessStatQueues = utils.BoolPointer(true) - } - */ + subsystems, err := cgrEv.FieldAsString(utils.CGRSubsystems) + if err != nil { + utils.Logger.Err(fmt.Sprintf("<%s> event: %s don't have cgr_subsystems variable", + utils.AsteriskAgent, utils.ToJSON(cgrEv))) + return nil + } + args.InitSession = strings.Index(subsystems, utils.MetaAccounts) != -1 + args.AllocateResources = strings.Index(subsystems, utils.MetaResources) != -1 + args.GetAttributes = strings.Index(subsystems, utils.MetaAttributes) != -1 + args.ProcessThresholds = strings.Index(subsystems, utils.MetaThresholds) != -1 + args.ProcessStats = strings.Index(subsystems, utils.MetaStats) != -1 return } @@ -341,23 +312,15 @@ func (smaEv *SMAsteriskEvent) V1TerminateSessionArgs(cgrEv utils.CGREvent) (args TerminateSession: true, CGREvent: cgrEv, } - /* - subsystems, has := kev[KamCGRSubsystems] - if !has { - return - } - if strings.Index(subsystems, utils.MetaAccounts) == -1 { - args.TerminateSession = false - } - if strings.Index(subsystems, utils.MetaResources) != -1 { - args.ReleaseResources = true - } - if strings.Index(subsystems, utils.MetaThresholds) != -1 { - args.ProcessThresholds = utils.BoolPointer(true) - } - if strings.Index(subsystems, utils.MetaStats) != -1 { - args.ProcessStatQueues = utils.BoolPointer(true) - } - */ + subsystems, err := cgrEv.FieldAsString(utils.CGRSubsystems) + if err != nil { + utils.Logger.Err(fmt.Sprintf("<%s> event: %s don't have cgr_subsystems variable", + utils.AsteriskAgent, utils.ToJSON(cgrEv))) + return nil + } + args.TerminateSession = strings.Index(subsystems, utils.MetaAccounts) != -1 + args.ReleaseResources = strings.Index(subsystems, utils.MetaResources) != -1 + args.ProcessThresholds = strings.Index(subsystems, utils.MetaThresholds) != -1 + args.ProcessStats = strings.Index(subsystems, utils.MetaStats) != -1 return } diff --git a/agents/asteriskagent.go b/agents/asteriskagent.go index f3c655b94..6c46d8a5b 100644 --- a/agents/asteriskagent.go +++ b/agents/asteriskagent.go @@ -229,6 +229,12 @@ func (sma *AsteriskAgent) handleStasisStart(ev *SMAsteriskEvent) { // Ussually channelUP func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { + // utils.Logger.Debug(fmt.Sprintf("#################handleChannelStateChange#####################")) + // utils.Logger.Debug(fmt.Sprintf("ev.ariEv : %+v", ev.ariEv)) + // utils.Logger.Debug(fmt.Sprintf("ev.asteriskIP : %+v", ev.asteriskIP)) + // utils.Logger.Debug(fmt.Sprintf("ev.cachedFields : %+v", ev.cachedFields)) + // utils.Logger.Debug(fmt.Sprintf("ev.Subsystems() : %+v", ev.Subsystems())) + if ev.ChannelState() != channelUp { return } @@ -238,6 +244,7 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { if !hasIt { // Not handled by us return } + sma.evCacheMux.Lock() err := ev.UpdateCGREvent(cgrEv) // Updates the event directly in the cache sma.evCacheMux.Unlock() @@ -247,8 +254,10 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { utils.AsteriskAgent, err.Error(), ev.ChannelID())) return } + // populate init session args initSessionArgs := ev.V1InitSessionArgs(*cgrEv) + if initSessionArgs == nil { utils.Logger.Err(fmt.Sprintf("<%s> event: %s cannot generate init session arguments", utils.AsteriskAgent, ev.ChannelID())) @@ -294,7 +303,6 @@ func (sma *AsteriskAgent) handleChannelDestroyed(ev *SMAsteriskEvent) { utils.AsteriskAgent, ev.ChannelID())) return } - var reply string if err := sma.smg.Call(utils.SessionSv1TerminateSession, tsArgs, &reply); err != nil { @@ -328,6 +336,27 @@ func (sma *AsteriskAgent) Call(serviceMethod string, args interface{}, reply int return utils.RPCCall(sma, serviceMethod, args, reply) } -func (fsa *AsteriskAgent) V1GetActiveSessionIDs(ignParam string, sessionIDs *[]*sessions.SessionID) (err error) { - return utils.ErrNotImplemented +func (sma *AsteriskAgent) V1GetActiveSessionIDs(ignParam string, + sessionIDs *[]*sessions.SessionID) (err error) { + utils.Logger.Debug(fmt.Sprintf("ASTERISK Enter in Sync session??")) + var sIDs []*sessions.SessionID + i := 0 + sma.evCacheMux.RLock() + originIds := make([]string, len(sma.eventsCache)) + for orgId := range sma.eventsCache { + originIds[i] = orgId + i++ + } + sma.evCacheMux.RUnlock() + fmt.Println("originIds : ", originIds) + fmt.Println("sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address : ", sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address) + for _, orgId := range originIds { + sIDs = append(sIDs, &sessions.SessionID{ + OriginHost: sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address, + OriginID: orgId}, + ) + } + *sessionIDs = sIDs + return + //return utils.ErrNotImplemented } diff --git a/agents/libhttpagent_test.go b/agents/libhttpagent_test.go index c35f83931..311a42ac7 100644 --- a/agents/libhttpagent_test.go +++ b/agents/libhttpagent_test.go @@ -130,4 +130,14 @@ func TestHttpXmlDPFieldAsInterface(t *testing.T) { } else if data != "37" { t.Errorf("expecting: 37, received: <%s>", data) } + if data, err := dP.FieldAsString([]string{"complete-success-notification", "callleg", "@calllegid"}); err != nil { + t.Error(err) + } else if data != "222146" { + t.Errorf("expecting: 222146, received: <%s>", data) + } + if data, err := dP.FieldAsString([]string{"complete-success-notification", "callleg[1]", "@calllegid"}); err != nil { + t.Error(err) + } else if data != "222147" { + t.Errorf("expecting: 222147, received: <%s>", data) + } } diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index c6ba5cebc..a23ebbfa5 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -315,15 +315,19 @@ func startAsteriskAgent(internalSMGChan chan rpcclient.RpcClientConnection, exit smgRpcConn := <-internalSMGChan internalSMGChan <- smgRpcConn birpcClnt := utils.NewBiRPCInternalClient(smgRpcConn.(*sessions.SMGeneric)) + var reply string for connIdx := range cfg.AsteriskAgentCfg().AsteriskConns { // Instantiate connections towards asterisk servers sma, err := agents.NewAsteriskAgent(cfg, connIdx, birpcClnt) if err != nil { - utils.Logger.Err(fmt.Sprintf(" error: %s!", err)) + utils.Logger.Err(fmt.Sprintf("<%s> error: %s!", utils.AsteriskAgent, err)) exitChan <- true return } + if err := birpcClnt.Call(utils.SessionSv1RegisterInternalBiJSONConn, "", &reply); err != nil { // for session sync + utils.Logger.Err(fmt.Sprintf("<%s> error: %s!", utils.AsteriskAgent, err)) + } if err = sma.ListenAndServe(); err != nil { - utils.Logger.Err(fmt.Sprintf(" runtime error: %s!", err)) + utils.Logger.Err(fmt.Sprintf("<%s> runtime error: %s!", utils.AsteriskAgent, err)) } } exitChan <- true diff --git a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf index b2368c755..7035c57a3 100755 --- a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf +++ b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf @@ -2,7 +2,7 @@ exten => _1XXX,1,NoOp() same => n,Set(CGRMaxSessionTime=0); use it to disconnect automatically the call if CGRateS is not active same => n,DumpChan() - same => n,Stasis(cgrates_auth,cgr_reqtype=*prepaid,cgr_supplier=supplier1,cgr_subsystems=*attributes;*accounts) + same => n,Stasis(cgrates_auth,cgr_reqtype=*prepaid,cgr_supplier=supplier1,cgr_subsystems=*attributes*sessions*suppliers*thresholds*stats*accounts*resources) same => n,Dial(PJSIP/${EXTEN},30,L(${CGRMaxSessionTime})) same => n,Hangup() diff --git a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/http.conf b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/http.conf index 12b5e2e8b..9d1f51885 100644 --- a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/http.conf +++ b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/http.conf @@ -1,4 +1,4 @@ [general] enabled = yes -bindaddr = 127.0.0.1 +bindaddr = 0.0.0.0 bindport = 8088 \ No newline at end of file diff --git a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf index e99652665..4db5b566c 100755 --- a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf +++ b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf @@ -1,7 +1,7 @@ [simpletrans] type=transport protocol=udp -bind=0.0.0.0 +bind=0.0.0.0:5080 [1001] type = endpoint @@ -27,16 +27,15 @@ password=CGRateS.org type = endpoint transport = simpletrans context = internal -aors = 1002 -auth = 1002 disallow = all allow = ulaw allow = alaw +aors = 1002 +auth = 1002 [1002] type = aor max_contacts = 5 -qualify_frequency = 0 [1002] type=auth diff --git a/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json b/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json index ba6c82254..778dbf093 100644 --- a/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json +++ b/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json @@ -28,21 +28,24 @@ "rals": { "enabled": true, "thresholds_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport":"*json"}, ], "stats_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport":"*json"}, ], "attributes_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport":"*json"}, ], }, "cdrs": { "enabled": true, + "sessions_conns": [ + {"address": "127.0.0.1:2012", "transport": "*json"} + ], "stats_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "sessions_cost_retries": 5, }, @@ -51,45 +54,47 @@ "sessions": { "enabled": true, "rals_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "cdrs_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "resources_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "suppliers_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "attributes_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "stats_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], "thresholds_conns": [ - {"address": "*internal"} + {"address": "127.0.0.1:2012", "transport": "*json"} ], - "debit_interval": "10s", + "debit_interval": "5s", + "channel_sync_interval":"7s", }, "asterisk_agent": { "enabled": true, "sessions_conns": [ - {"address": "*internal"} + {"address": "*internal"} ], "create_cdr": true, "asterisk_conns":[ - {"address": "192.168.56.103:8088", "user": "cgrates", + {"address": "192.168.56.203:8088", "user": "cgrates", "password": "CGRateS.org", "connect_attempts": 3,"reconnects": 10} ], }, "attributes": { - "enabled": true, + "enabled": true, + "string_indexed_fields": ["Account"], }, @@ -98,6 +103,7 @@ "thresholds_conns": [ {"address": "*internal"} ], + "string_indexed_fields": ["Account"], }, @@ -106,11 +112,13 @@ "thresholds_conns": [ {"address": "*internal"} ], + "string_indexed_fields": ["Account","RunID","Destination"], }, "thresholds": { "enabled": true, + "string_indexed_fields": ["Account"], }, @@ -125,6 +133,7 @@ "stats_conns": [ {"address": "*internal"} ], + "string_indexed_fields": ["Account"], }, diff --git a/general_tests/tutorial_calls_test.go b/general_tests/tutorial_calls_test.go index cb897ab28..7a160a653 100755 --- a/general_tests/tutorial_calls_test.go +++ b/general_tests/tutorial_calls_test.go @@ -104,14 +104,12 @@ func TestOpensipsCalls(t *testing.T) { } } -/* Need to be checked func TestAsteriskCalls(t *testing.T) { optConf = utils.Asterisk for _, stest := range sTestsCalls { t.Run("", stest) } } -*/ func testCallInitCfg(t *testing.T) { // Init config first @@ -243,7 +241,7 @@ func testCallRestartFS(t *testing.T) { // Connect rpc client to rater func testCallRpcConn(t *testing.T) { var err error - tutorialCallsRpc, err = jsonrpc.Dial("tcp", tutorialCallsCfg.RPCJSONListen) // We connect over JSON so we can also troubleshoot if needed + tutorialCallsRpc, err = jsonrpc.Dial("tcp", tutorialCallsCfg.ListenCfg().RPCJSONListen) // We connect over JSON so we can also troubleshoot if needed if err != nil { t.Fatal(err) } diff --git a/sessions/sessions.go b/sessions/sessions.go index 15643a6df..48e380750 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -2322,6 +2322,7 @@ func (smg *SMGeneric) OnBiJSONDisconnect(c *rpc2.Client) { } func (smg *SMGeneric) syncSessions() { + utils.Logger.Debug("Enter in sync sessions ????") var rpcClnts []rpcclient.RpcClientConnection for _, conn := range smg.intBiJSONConns { rpcClnts = append(rpcClnts, conn) @@ -2344,6 +2345,7 @@ func (smg *SMGeneric) syncSessions() { } } } + utils.Logger.Debug(fmt.Sprintf("queried CGRIDS : %+v", queriedCGRIDs)) var toBeRemoved []string smg.aSessionsMux.RLock() for cgrid := range smg.activeSessions { From 5b94c21f732a42187e453a02190383d8fe34234d Mon Sep 17 00:00:00 2001 From: TeoV Date: Wed, 14 Nov 2018 07:05:17 -0500 Subject: [PATCH 05/12] Update stats from tutorial with ttl -1 --- agents/asteriskagent.go | 13 +--- data/tariffplans/tutorial/Attributes.csv | 1 + data/tariffplans/tutorial/Resources.csv | 2 +- data/tariffplans/tutorial/Stats.csv | 4 +- .../kamevapi/cgrates/etc/cgrates/cgrates.json | 8 +-- engine/libstats.go | 6 ++ general_tests/tutorial_calls_test.go | 68 ++++++++++++++----- sessions/sessions.go | 1 - 8 files changed, 65 insertions(+), 38 deletions(-) diff --git a/agents/asteriskagent.go b/agents/asteriskagent.go index 6c46d8a5b..ccef7ab59 100644 --- a/agents/asteriskagent.go +++ b/agents/asteriskagent.go @@ -229,12 +229,6 @@ func (sma *AsteriskAgent) handleStasisStart(ev *SMAsteriskEvent) { // Ussually channelUP func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { - // utils.Logger.Debug(fmt.Sprintf("#################handleChannelStateChange#####################")) - // utils.Logger.Debug(fmt.Sprintf("ev.ariEv : %+v", ev.ariEv)) - // utils.Logger.Debug(fmt.Sprintf("ev.asteriskIP : %+v", ev.asteriskIP)) - // utils.Logger.Debug(fmt.Sprintf("ev.cachedFields : %+v", ev.cachedFields)) - // utils.Logger.Debug(fmt.Sprintf("ev.Subsystems() : %+v", ev.Subsystems())) - if ev.ChannelState() != channelUp { return } @@ -338,7 +332,6 @@ func (sma *AsteriskAgent) Call(serviceMethod string, args interface{}, reply int func (sma *AsteriskAgent) V1GetActiveSessionIDs(ignParam string, sessionIDs *[]*sessions.SessionID) (err error) { - utils.Logger.Debug(fmt.Sprintf("ASTERISK Enter in Sync session??")) var sIDs []*sessions.SessionID i := 0 sma.evCacheMux.RLock() @@ -348,15 +341,13 @@ func (sma *AsteriskAgent) V1GetActiveSessionIDs(ignParam string, i++ } sma.evCacheMux.RUnlock() - fmt.Println("originIds : ", originIds) - fmt.Println("sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address : ", sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address) for _, orgId := range originIds { sIDs = append(sIDs, &sessions.SessionID{ - OriginHost: sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address, + OriginHost: strings.Split(sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address, ":")[0], OriginID: orgId}, ) } *sessionIDs = sIDs return - //return utils.ErrNotImplemented + } diff --git a/data/tariffplans/tutorial/Attributes.csv b/data/tariffplans/tutorial/Attributes.csv index 2e8f6f686..a87504e9c 100644 --- a/data/tariffplans/tutorial/Attributes.csv +++ b/data/tariffplans/tutorial/Attributes.csv @@ -15,6 +15,7 @@ cgrates.org,ATTR_1003_SESSIONAUTH,*sessions,*string:Account:1003,,Password,*any, cgrates.org,ATTR_1003_SESSIONAUTH,,,,RequestType,*any,*prepaid,true,, cgrates.org,ATTR_1003_SESSIONAUTH,,,,PaypalAccount,*any,cgrates@paypal.com,true,, cgrates.org,ATTR_1003_SESSIONAUTH,,,,LCRProfile,*any,premium_cli,true,, +cgrates.org,ATTR_1003_SESSIONAUTH,,,,ResourceAllocation,*any,"ResGroup1",true,, cgrates.org,ATTR_1006_ALIAS,*any,*string:SubscriberId:1006,,Account,*any,1001,true,false,10 cgrates.org,ATTR_1006_ALIAS,*any,,,RequestType,*any,*prepaid,true,, diff --git a/data/tariffplans/tutorial/Resources.csv b/data/tariffplans/tutorial/Resources.csv index 60af73072..945587cd6 100644 --- a/data/tariffplans/tutorial/Resources.csv +++ b/data/tariffplans/tutorial/Resources.csv @@ -1,2 +1,2 @@ #Tenant[0],Id[1],FilterIDs[2],ActivationInterval[3],TTL[4],Limit[5],AllocationMessage[6],Blocker[7],Stored[8],Weight[9],ThresholdIDs[10] -cgrates.org,ResGroup1,FLTR_RES,2014-07-29T15:00:00Z,3600s,7,,false,true,10, \ No newline at end of file +cgrates.org,ResGroup1,FLTR_RES,2014-07-29T15:00:00Z,-1,7,,false,true,10,*none \ No newline at end of file diff --git a/data/tariffplans/tutorial/Stats.csv b/data/tariffplans/tutorial/Stats.csv index a84b1490d..3323cb960 100644 --- a/data/tariffplans/tutorial/Stats.csv +++ b/data/tariffplans/tutorial/Stats.csv @@ -1,3 +1,3 @@ #Tenant[0],Id[1],FilterIDs[2],ActivationInterval[3],QueueLength[4],TTL[5],Metrics[6],MetricParams[7],Blocker[8],Stored[9],Weight[10],MinItems[11],ThresholdIDs[12] -cgrates.org,Stats2,FLTR_ACNT_1001_1002,2014-07-29T15:00:00Z,100,1s,*tcc;*tcd,,false,true,30,0, -cgrates.org,Stats2_1,FLTR_ACNT_1003_1001,2014-07-29T15:00:00Z,100,1s,*tcc;*tcd,,false,true,30,0, +cgrates.org,Stats2,FLTR_ACNT_1001_1002,2014-07-29T15:00:00Z,100,-1,*tcc;*tcd,,false,true,30,0,*none +cgrates.org,Stats2_1,FLTR_ACNT_1003_1001,2014-07-29T15:00:00Z,100,-1,*tcc;*tcd,,false,true,30,0,*none diff --git a/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json b/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json index d947b5357..3111e6171 100644 --- a/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json +++ b/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json @@ -5,6 +5,7 @@ "general": { "log_level": 7, + "node_id":"CGRKamailio", }, @@ -33,9 +34,6 @@ "stats_conns": [ {"address": "127.0.0.1:2012", "transport": "*json"} ], - "attributes_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} - ], }, @@ -117,7 +115,7 @@ "thresholds_conns": [ {"address": "*internal"} ], - "string_indexed_fields": ["Account"], + "string_indexed_fields": ["Account","RunID","Destination"], }, @@ -143,4 +141,4 @@ }, -} +} \ No newline at end of file diff --git a/engine/libstats.go b/engine/libstats.go index 74b8fdb9c..80e2a1d6f 100644 --- a/engine/libstats.go +++ b/engine/libstats.go @@ -136,9 +136,13 @@ func (sq *StatQueue) TenantID() string { // ProcessEvent processes a utils.CGREvent, returns true if processed func (sq *StatQueue) ProcessEvent(ev *utils.CGREvent) (err error) { + fmt.Println("sq before ", utils.ToJSON(sq)) sq.remExpired() + fmt.Println("sq after rem expired ", utils.ToJSON(sq)) sq.remOnQueueLength() + fmt.Println("sq after rem queue lengt ", utils.ToJSON(sq)) sq.addStatEvent(ev) + fmt.Println("sq after radd event ", utils.ToJSON(sq)) return } @@ -185,9 +189,11 @@ func (sq *StatQueue) remOnQueueLength() { // addStatEvent computes metrics for an event func (sq *StatQueue) addStatEvent(ev *utils.CGREvent) { var expTime *time.Time + fmt.Println("sq.ttl : ", sq.ttl) if sq.ttl != nil { expTime = utils.TimePointer(time.Now().Add(*sq.ttl)) } + fmt.Println("expTime : ", expTime) sq.SQItems = append(sq.SQItems, struct { EventID string diff --git a/general_tests/tutorial_calls_test.go b/general_tests/tutorial_calls_test.go index 7a160a653..f34364070 100755 --- a/general_tests/tutorial_calls_test.go +++ b/general_tests/tutorial_calls_test.go @@ -39,7 +39,7 @@ import ( var tutorialCallsCfg *config.CGRConfig var tutorialCallsRpc *rpc.Client var tutorialCallsPjSuaListener *os.File -var waitRater = flag.Int("wait_rater", 100, "Number of miliseconds to wait for rater to start and cache") +var waitRater = flag.Int("wait_rater", 1000, "Number of miliseconds to wait for rater to start and cache") var dataDir = flag.String("data_dir", "/usr/share/cgrates", "CGR data dir path here") var fsConfig = flag.String("fsConfig", "/usr/share/cgrates/tutorials/fs_evsock", "FreeSwitch tutorial folder") var kamConfig = flag.String("kamConfig", "/usr/share/cgrates/tutorials/kamevapi", "Kamailio tutorial folder") @@ -72,6 +72,7 @@ var sTestsCalls = []func(t *testing.T){ testCallAccount1001, testCall1001Cdrs, testCall1002Cdrs, + testCall1003Cdrs, testCallStatMetrics, testCallCheckResourceRelease, testCallCheckThreshold1001After, @@ -245,7 +246,6 @@ func testCallRpcConn(t *testing.T) { if err != nil { t.Fatal(err) } - time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time for scheduler to execute topups } // Load the tariff plan, creating accounts and their balances @@ -364,6 +364,7 @@ func testCallStartPjsuaListener(t *testing.T) { acnts, 5070, time.Duration(*waitRater)*time.Millisecond); err != nil { t.Fatal(err) } + time.Sleep(1 * time.Second) } // Call from 1001 (prepaid) to 1002 @@ -373,7 +374,8 @@ func testCallCall1001To1002(t *testing.T) { "sip:1002@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(67)*time.Second, 5071); err != nil { t.Fatal(err) } - time.Sleep(1 * time.Second) + // give time to session to start so we can check it + time.Sleep(time.Second) } // GetActiveSessions @@ -430,6 +432,7 @@ func testCallCall1003To1001(t *testing.T) { t.Fatal(err) } time.Sleep(22 * time.Second) + // after this call from 1001 to 1003 and call from 1003 to 1001 should be done } // Call from 1003 (prepaid) to 1001 for 15 seconds @@ -439,6 +442,7 @@ func testCallCall1003To1001SecondTime(t *testing.T) { "sip:1001@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(15)*time.Second, 5075); err != nil { t.Fatal(err) } + time.Sleep(time.Second) } // Check if the resource was Allocated @@ -463,11 +467,12 @@ func testCallCheckResourceAllocation(t *testing.T) { t.Errorf("Unexpected resource: %+v", utils.ToJSON(r)) } } + // Allow calls to finish before start querying the results + time.Sleep(time.Duration(45) * time.Second) } // Make sure account was debited properly func testCallAccount1001(t *testing.T) { - time.Sleep(time.Duration(60) * time.Second) // Allow calls to finish before start querying the results var reply *engine.Account attrs := &utils.AttrGetAccount{Tenant: "cgrates.org", Account: "1001"} if err := tutorialCallsRpc.Call("ApierV2.GetAccount", attrs, &reply); err != nil { @@ -514,7 +519,8 @@ func testCall1001Cdrs(t *testing.T) { // Make sure account was debited properly func testCall1002Cdrs(t *testing.T) { var reply []*engine.ExternalCDR - req := utils.RPCCDRsFilter{RunIDs: []string{utils.META_DEFAULT}, Accounts: []string{"1002"}, DestinationPrefixes: []string{"1001"}} + req := utils.RPCCDRsFilter{RunIDs: []string{utils.META_DEFAULT}, + Accounts: []string{"1002"}, DestinationPrefixes: []string{"1001"}} if err := tutorialCallsRpc.Call("ApierV2.GetCdrs", req, &reply); err != nil { t.Error("Unexpected error: ", err.Error()) } else if len(reply) != 1 { @@ -532,11 +538,36 @@ func testCall1002Cdrs(t *testing.T) { } } +// Make sure account was debited properly +func testCall1003Cdrs(t *testing.T) { + var reply []*engine.ExternalCDR + req := utils.RPCCDRsFilter{RunIDs: []string{utils.META_DEFAULT}, + Accounts: []string{"1003"}, DestinationPrefixes: []string{"1001"}} + if err := tutorialCallsRpc.Call("ApierV2.GetCdrs", req, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(reply) != 2 { + t.Error("Unexpected number of CDRs returned: ", len(reply)) + } else { + for _, cdr := range reply { + if cdr.RequestType != utils.META_PREPAID { + t.Errorf("Unexpected RequestType for CDR: %+v", cdr.RequestType) + } + if cdr.Usage != "15s" && cdr.Usage != "20s" { // Usage as seconds + t.Errorf("Unexpected Usage for CDR: %+v", cdr.Usage) + } + if cdr.CostSource != utils.MetaSessionS { + t.Errorf("Unexpected CostSource for CDR: %+v", cdr.CostSource) + } + + } + } +} + func testCallStatMetrics(t *testing.T) { var metrics map[string]string firstStatMetrics1 := map[string]string{ - utils.MetaTCC: "1.35009", - utils.MetaTCD: "2m25s", + utils.MetaTCC: "1.22009", + utils.MetaTCD: "2m12s", } firstStatMetrics2 := map[string]string{ utils.MetaTCC: "1.34009", @@ -614,19 +645,19 @@ func testCallSyncSessions(t *testing.T) { var reply *[]*sessions.ActiveSession // activeSessions shouldn't be active if err := tutorialCallsRpc.Call(utils.SessionSv1GetActiveSessions, - &map[string]string{}, &reply); err.Error() != utils.ErrNotFound.Error() { - t.Error("Got error on SessionSv1.GetActiveSessions: ", err.Error()) + &map[string]string{}, &reply); err == nil || err.Error() != utils.ErrNotFound.Error() { + t.Error("Got error on SessionSv1.GetActiveSessions: ", err) } // 1001 call 1002 stop the call after 12 seconds if err := engine.PjsuaCallUri( &engine.PjsuaAccount{Id: "sip:1001@127.0.0.1", Username: "1001", Password: "CGRateS.org", Realm: "*"}, - "sip:1002@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(120)*time.Second, 5071); err != nil { + "sip:1002@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(120)*time.Second, 5076); err != nil { t.Fatal(err) } // 1001 call 1003 stop the call after 11 seconds if err := engine.PjsuaCallUri( &engine.PjsuaAccount{Id: "sip:1001@127.0.0.1", Username: "1001", Password: "CGRateS.org", Realm: "*"}, - "sip:1003@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(120)*time.Second, 5073); err != nil { + "sip:1003@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(120)*time.Second, 5077); err != nil { t.Fatal(err) } time.Sleep(1 * time.Second) @@ -635,21 +666,19 @@ func testCallSyncSessions(t *testing.T) { &map[string]string{}, &reply); err != nil { t.Error("Got error on SessionSv1.GetActiveSessions: ", err.Error()) } else if len(*reply) != 2 { - t.Errorf("expecting 2, received reply: %+v", utils.ToJSON(reply)) + t.Errorf("expecting 2 active sessions, received: %+v", utils.ToJSON(reply)) } //check if resource was allocated for 2 calls(1001->1002;1001->1003) var rs *engine.Resources args := &utils.ArgRSv1ResourceUsage{ CGREvent: utils.CGREvent{ Tenant: "cgrates.org", - ID: "ResourceAllocation", + ID: "AllocateResource", Event: map[string]interface{}{ utils.Account: "1001", utils.Subject: "1001", utils.Destination: "1002"}, - }, - Units: 1, - } + }} if err := tutorialCallsRpc.Call(utils.ResourceSv1GetResourcesForEvent, args, &rs); err != nil { t.Error(err) } else if len(*rs) != 1 { @@ -684,8 +713,8 @@ func testCallSyncSessions(t *testing.T) { // activeSessions shouldn't be active if err := tutorialCallsRpc.Call(utils.SessionSv1GetActiveSessions, - &map[string]string{}, &reply); err.Error() != utils.ErrNotFound.Error() { - t.Error("Got error on SessionSv1.GetActiveSessions: ", err.Error()) + &map[string]string{}, &reply); err == nil || err.Error() != utils.ErrNotFound.Error() { + t.Error("Got error on SessionSv1.GetActiveSessions: ", err) } var sourceForCDR string @@ -697,6 +726,9 @@ func testCallSyncSessions(t *testing.T) { case utils.Kamailio: sourceForCDR = utils.KamailioAgent numberOfCDR = 3 + case utils.Asterisk: + sourceForCDR = "" + numberOfCDR = 3 } // verify cdr var rplCdrs []*engine.ExternalCDR diff --git a/sessions/sessions.go b/sessions/sessions.go index 48e380750..f5c8f1865 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -2345,7 +2345,6 @@ func (smg *SMGeneric) syncSessions() { } } } - utils.Logger.Debug(fmt.Sprintf("queried CGRIDS : %+v", queriedCGRIDs)) var toBeRemoved []string smg.aSessionsMux.RLock() for cgrid := range smg.activeSessions { From ba92a5ca4b25cb3f7da85daaab7190b24b2f8c4e Mon Sep 17 00:00:00 2001 From: TeoV Date: Wed, 14 Nov 2018 07:13:30 -0500 Subject: [PATCH 06/12] Remove debugging logs --- engine/libstats.go | 6 ------ sessions/sessions.go | 1 - 2 files changed, 7 deletions(-) diff --git a/engine/libstats.go b/engine/libstats.go index 80e2a1d6f..74b8fdb9c 100644 --- a/engine/libstats.go +++ b/engine/libstats.go @@ -136,13 +136,9 @@ func (sq *StatQueue) TenantID() string { // ProcessEvent processes a utils.CGREvent, returns true if processed func (sq *StatQueue) ProcessEvent(ev *utils.CGREvent) (err error) { - fmt.Println("sq before ", utils.ToJSON(sq)) sq.remExpired() - fmt.Println("sq after rem expired ", utils.ToJSON(sq)) sq.remOnQueueLength() - fmt.Println("sq after rem queue lengt ", utils.ToJSON(sq)) sq.addStatEvent(ev) - fmt.Println("sq after radd event ", utils.ToJSON(sq)) return } @@ -189,11 +185,9 @@ func (sq *StatQueue) remOnQueueLength() { // addStatEvent computes metrics for an event func (sq *StatQueue) addStatEvent(ev *utils.CGREvent) { var expTime *time.Time - fmt.Println("sq.ttl : ", sq.ttl) if sq.ttl != nil { expTime = utils.TimePointer(time.Now().Add(*sq.ttl)) } - fmt.Println("expTime : ", expTime) sq.SQItems = append(sq.SQItems, struct { EventID string diff --git a/sessions/sessions.go b/sessions/sessions.go index f5c8f1865..15643a6df 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -2322,7 +2322,6 @@ func (smg *SMGeneric) OnBiJSONDisconnect(c *rpc2.Client) { } func (smg *SMGeneric) syncSessions() { - utils.Logger.Debug("Enter in sync sessions ????") var rpcClnts []rpcclient.RpcClientConnection for _, conn := range smg.intBiJSONConns { rpcClnts = append(rpcClnts, conn) From 88749338afce19f07664e1f093c8df903f7a5410 Mon Sep 17 00:00:00 2001 From: TeoV Date: Wed, 14 Nov 2018 09:03:10 -0500 Subject: [PATCH 07/12] Add logs --- data/tariffplans/tutorial/Attributes.csv | 1 - .../kamevapi/cgrates/etc/cgrates/cgrates.json | 2 -- general_tests/tutorial_calls_test.go | 28 ++++++++++++++----- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/data/tariffplans/tutorial/Attributes.csv b/data/tariffplans/tutorial/Attributes.csv index a87504e9c..2e8f6f686 100644 --- a/data/tariffplans/tutorial/Attributes.csv +++ b/data/tariffplans/tutorial/Attributes.csv @@ -15,7 +15,6 @@ cgrates.org,ATTR_1003_SESSIONAUTH,*sessions,*string:Account:1003,,Password,*any, cgrates.org,ATTR_1003_SESSIONAUTH,,,,RequestType,*any,*prepaid,true,, cgrates.org,ATTR_1003_SESSIONAUTH,,,,PaypalAccount,*any,cgrates@paypal.com,true,, cgrates.org,ATTR_1003_SESSIONAUTH,,,,LCRProfile,*any,premium_cli,true,, -cgrates.org,ATTR_1003_SESSIONAUTH,,,,ResourceAllocation,*any,"ResGroup1",true,, cgrates.org,ATTR_1006_ALIAS,*any,*string:SubscriberId:1006,,Account,*any,1001,true,false,10 cgrates.org,ATTR_1006_ALIAS,*any,,,RequestType,*any,*prepaid,true,, diff --git a/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json b/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json index 3111e6171..47c99d8f6 100644 --- a/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json +++ b/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json @@ -106,7 +106,6 @@ {"address": "*internal"} ], "string_indexed_fields": ["Account"], - "prefix_indexed_fields": ["Destination"], }, @@ -137,7 +136,6 @@ {"address": "*internal"} ], "string_indexed_fields": ["Account"], - "prefix_indexed_fields": ["Destination"], }, diff --git a/general_tests/tutorial_calls_test.go b/general_tests/tutorial_calls_test.go index f34364070..7e3443da5 100755 --- a/general_tests/tutorial_calls_test.go +++ b/general_tests/tutorial_calls_test.go @@ -77,7 +77,7 @@ var sTestsCalls = []func(t *testing.T){ testCallCheckResourceRelease, testCallCheckThreshold1001After, testCallCheckThreshold1002After, - testCallSyncSessions, + //testCallSyncSessions, testCallStopPjsuaListener, testCallStopCgrEngine, testCallStopFS, @@ -424,6 +424,11 @@ func testCallCall1001To1003(t *testing.T) { } } +// 1001 -> 1002 67s +// 1002 -> 1001 65s +// 1001 -> 1003 12s +// here we have 3 units of resource allocation + // Call from 1003 (prepaid) to 1001 for 20 seconds func testCallCall1003To1001(t *testing.T) { if err := engine.PjsuaCallUri( @@ -431,12 +436,17 @@ func testCallCall1003To1001(t *testing.T) { "sip:1001@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(20)*time.Second, 5074); err != nil { t.Fatal(err) } - time.Sleep(22 * time.Second) // after this call from 1001 to 1003 and call from 1003 to 1001 should be done } +// 1001 -> 1002 45s +// 1002 -> 1001 43s +// 1003 -> 1001 20s +// 1 unit was release and 1 units was accolated (now we have 3 units allocated ) + // Call from 1003 (prepaid) to 1001 for 15 seconds func testCallCall1003To1001SecondTime(t *testing.T) { + time.Sleep(22 * time.Second) if err := engine.PjsuaCallUri( &engine.PjsuaAccount{Id: "sip:1003@127.0.0.1", Username: "1003", Password: "CGRateS.org", Realm: "*"}, "sip:1001@127.0.0.1", "sip:127.0.0.1:5080", time.Duration(15)*time.Second, 5075); err != nil { @@ -445,6 +455,11 @@ func testCallCall1003To1001SecondTime(t *testing.T) { time.Sleep(time.Second) } +// 1001 -> 1002 22s +// 1002 -> 1001 20s +// 1003 -> 1001 15s (new call) +// 3 units allocated + // Check if the resource was Allocated func testCallCheckResourceAllocation(t *testing.T) { var rs *engine.Resources @@ -463,12 +478,12 @@ func testCallCheckResourceAllocation(t *testing.T) { t.Errorf("Resources: %+v", utils.ToJSON(rs)) } for _, r := range *rs { - if r.ID == "ResGroup1" && (len(r.Usages) != 2 || len(r.TTLIdx) != 2) { + if r.ID == "ResGroup1" && len(r.Usages) != 3 { t.Errorf("Unexpected resource: %+v", utils.ToJSON(r)) } } // Allow calls to finish before start querying the results - time.Sleep(time.Duration(45) * time.Second) + time.Sleep(time.Duration(30) * time.Second) } // Make sure account was debited properly @@ -610,9 +625,8 @@ func testCallCheckResourceRelease(t *testing.T) { t.Errorf("Resources: %+v", rs) } for _, r := range *rs { - if r.ID == "ResGroup1" && - (len(r.Usages) != 0 || len(r.TTLIdx) != 0) { - t.Errorf("Unexpected resource: %+v", r) + if r.ID == "ResGroup1" && len(r.Usages) != 0 { + t.Errorf("Unexpected resource: %+v", utils.ToJSON(r)) } } } From 1b371419db859a4e49d6b65832b51cb22b351652 Mon Sep 17 00:00:00 2001 From: TeoV Date: Wed, 14 Nov 2018 10:41:27 -0500 Subject: [PATCH 08/12] Update call test for freeswitch and kamailio --- agents/asterisk_event.go | 1 + data/tariffplans/tutorial/Filters.csv | 2 +- .../asterisk/etc/asterisk/pjsip.conf | 1 + general_tests/tutorial_calls_test.go | 25 ++++--------------- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/agents/asterisk_event.go b/agents/asterisk_event.go index fa52c14b8..d385c7747 100644 --- a/agents/asterisk_event.go +++ b/agents/asterisk_event.go @@ -245,6 +245,7 @@ func (smaEv *SMAsteriskEvent) AsMapStringInterface() (mp map[string]interface{}) for extraKey, extraVal := range smaEv.ExtraParameters() { // Append extraParameters mp[extraKey] = extraVal } + mp[utils.Source] = utils.KamailioAgent return } diff --git a/data/tariffplans/tutorial/Filters.csv b/data/tariffplans/tutorial/Filters.csv index 3b62ee7ef..143bd00b7 100644 --- a/data/tariffplans/tutorial/Filters.csv +++ b/data/tariffplans/tutorial/Filters.csv @@ -4,7 +4,7 @@ cgrates.org,FLTR_DST_FS,*string,Account,1001,2014-07-29T15:00:00Z cgrates.org,FLTR_DST_FS,*destinations,Destination,DST_FS, cgrates.org,FLTR_ACNT_1001_1002,*string,Account,1001;1002,2014-07-29T15:00:00Z cgrates.org,FLTR_ACNT_1001_1002,*string,RunID,*default, -cgrates.org,FLTR_ACNT_1001_1003,*string,Destination,1001;1002, +cgrates.org,FLTR_ACNT_1001_1002,*string,Destination,1001;1002;1003, cgrates.org,FLTR_ACNT_1001,*string,Account,1001,2014-07-29T15:00:00Z cgrates.org,FLTR_ACNT_1002,*string,Account,1002,2014-07-29T15:00:00Z cgrates.org,FLTR_ACNT_1003,*string,Account,1003,2014-07-29T15:00:00Z diff --git a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf index 4db5b566c..a545acda4 100755 --- a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf +++ b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/pjsip.conf @@ -36,6 +36,7 @@ auth = 1002 [1002] type = aor max_contacts = 5 +qualify_frequency = 0 [1002] type=auth diff --git a/general_tests/tutorial_calls_test.go b/general_tests/tutorial_calls_test.go index 7e3443da5..9c524b319 100755 --- a/general_tests/tutorial_calls_test.go +++ b/general_tests/tutorial_calls_test.go @@ -77,7 +77,7 @@ var sTestsCalls = []func(t *testing.T){ testCallCheckResourceRelease, testCallCheckThreshold1001After, testCallCheckThreshold1002After, - //testCallSyncSessions, + testCallSyncSessions, testCallStopPjsuaListener, testCallStopCgrEngine, testCallStopFS, @@ -424,11 +424,6 @@ func testCallCall1001To1003(t *testing.T) { } } -// 1001 -> 1002 67s -// 1002 -> 1001 65s -// 1001 -> 1003 12s -// here we have 3 units of resource allocation - // Call from 1003 (prepaid) to 1001 for 20 seconds func testCallCall1003To1001(t *testing.T) { if err := engine.PjsuaCallUri( @@ -439,11 +434,6 @@ func testCallCall1003To1001(t *testing.T) { // after this call from 1001 to 1003 and call from 1003 to 1001 should be done } -// 1001 -> 1002 45s -// 1002 -> 1001 43s -// 1003 -> 1001 20s -// 1 unit was release and 1 units was accolated (now we have 3 units allocated ) - // Call from 1003 (prepaid) to 1001 for 15 seconds func testCallCall1003To1001SecondTime(t *testing.T) { time.Sleep(22 * time.Second) @@ -455,11 +445,6 @@ func testCallCall1003To1001SecondTime(t *testing.T) { time.Sleep(time.Second) } -// 1001 -> 1002 22s -// 1002 -> 1001 20s -// 1003 -> 1001 15s (new call) -// 3 units allocated - // Check if the resource was Allocated func testCallCheckResourceAllocation(t *testing.T) { var rs *engine.Resources @@ -483,7 +468,7 @@ func testCallCheckResourceAllocation(t *testing.T) { } } // Allow calls to finish before start querying the results - time.Sleep(time.Duration(30) * time.Second) + time.Sleep(time.Duration(50) * time.Second) } // Make sure account was debited properly @@ -699,7 +684,7 @@ func testCallSyncSessions(t *testing.T) { t.Errorf("Resources: %+v", utils.ToJSON(rs)) } for _, r := range *rs { - if r.ID == "ResGroup1" && (len(r.Usages) != 2 || len(r.TTLIdx) != 2) { + if r.ID == "ResGroup1" && len(r.Usages) != 2 { t.Errorf("Unexpected resource: %+v", utils.ToJSON(r)) } } @@ -727,7 +712,7 @@ func testCallSyncSessions(t *testing.T) { // activeSessions shouldn't be active if err := tutorialCallsRpc.Call(utils.SessionSv1GetActiveSessions, - &map[string]string{}, &reply); err == nil || err.Error() != utils.ErrNotFound.Error() { + &map[string]string{}, &reply); err != nil && err.Error() != utils.ErrNotFound.Error() { t.Error("Got error on SessionSv1.GetActiveSessions: ", err) } @@ -741,7 +726,7 @@ func testCallSyncSessions(t *testing.T) { sourceForCDR = utils.KamailioAgent numberOfCDR = 3 case utils.Asterisk: - sourceForCDR = "" + sourceForCDR = utils.AsteriskAgent numberOfCDR = 3 } // verify cdr From 40e40710a1adde0267f936b3b348771805a9ca73 Mon Sep 17 00:00:00 2001 From: TeoV Date: Thu, 15 Nov 2018 03:52:41 -0500 Subject: [PATCH 09/12] Sync cgrates.json in tutorial folder --- agents/asterisk_event.go | 6 +- .../asterisk/etc/asterisk/extensions.conf | 2 +- .../cgrates/etc/cgrates/cgrates.json | 52 +++++++--------- .../cgrates/etc/cgrates/cgrates.json | 59 +++++++------------ .../kamevapi/cgrates/etc/cgrates/cgrates.json | 45 ++++++-------- general_tests/tutorial_calls_test.go | 27 ++++++--- 6 files changed, 83 insertions(+), 108 deletions(-) diff --git a/agents/asterisk_event.go b/agents/asterisk_event.go index d385c7747..a51986b62 100644 --- a/agents/asterisk_event.go +++ b/agents/asterisk_event.go @@ -245,7 +245,7 @@ func (smaEv *SMAsteriskEvent) AsMapStringInterface() (mp map[string]interface{}) for extraKey, extraVal := range smaEv.ExtraParameters() { // Append extraParameters mp[extraKey] = extraVal } - mp[utils.Source] = utils.KamailioAgent + mp[utils.Source] = utils.AsteriskAgent return } @@ -275,7 +275,9 @@ func (smaEv *SMAsteriskEvent) V1AuthorizeArgs() (args *sessions.V1AuthorizeArgs) GetMaxUsage: true, CGREvent: *cgrEv, } - + if smaEv.Subsystems() == utils.EmptyString { + return + } args.GetMaxUsage = strings.Index(smaEv.Subsystems(), utils.MetaAccounts) != -1 args.AuthorizeResources = strings.Index(smaEv.Subsystems(), utils.MetaResources) != -1 args.GetSuppliers = strings.Index(smaEv.Subsystems(), utils.MetaSuppliers) != -1 diff --git a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf index 7035c57a3..7c3c8da6c 100755 --- a/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf +++ b/data/tutorials/asterisk_ari/asterisk/etc/asterisk/extensions.conf @@ -2,7 +2,7 @@ exten => _1XXX,1,NoOp() same => n,Set(CGRMaxSessionTime=0); use it to disconnect automatically the call if CGRateS is not active same => n,DumpChan() - same => n,Stasis(cgrates_auth,cgr_reqtype=*prepaid,cgr_supplier=supplier1,cgr_subsystems=*attributes*sessions*suppliers*thresholds*stats*accounts*resources) + same => n,Stasis(cgrates_auth,cgr_reqtype=*prepaid,cgr_supplier=supplier1,cgr_subsystems=*accounts*attributes*resources*stats*suppliers*thresholds) same => n,Dial(PJSIP/${EXTEN},30,L(${CGRMaxSessionTime})) same => n,Hangup() diff --git a/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json b/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json index 778dbf093..1a8cfb9d7 100644 --- a/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json +++ b/data/tutorials/asterisk_ari/cgrates/etc/cgrates/cgrates.json @@ -5,6 +5,7 @@ "general": { "log_level": 7, + "node_id":"CGRAsterisk", }, @@ -28,13 +29,10 @@ "rals": { "enabled": true, "thresholds_conns": [ - {"address": "127.0.0.1:2012", "transport":"*json"}, + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport":"*json"}, - ], - "attributes_conns": [ - {"address": "127.0.0.1:2012", "transport":"*json"}, + {"address": "127.0.0.1:2012", "transport": "*json"}, ], }, @@ -42,10 +40,10 @@ "cdrs": { "enabled": true, "sessions_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "sessions_cost_retries": 5, }, @@ -54,25 +52,25 @@ "sessions": { "enabled": true, "rals_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "cdrs_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "resources_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "suppliers_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "attributes_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "thresholds_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "debit_interval": "5s", "channel_sync_interval":"7s", @@ -80,15 +78,15 @@ "asterisk_agent": { - "enabled": true, - "sessions_conns": [ - {"address": "*internal"} - ], - "create_cdr": true, - "asterisk_conns":[ - {"address": "192.168.56.203:8088", "user": "cgrates", + "enabled": true, + "asterisk_conns":[ + {"address": "192.168.56.203:8088", "user": "cgrates", "password": "CGRateS.org", "connect_attempts": 3,"reconnects": 10} ], + "sessions_conns": [ + {"address": "*internal"}, + ], + "create_cdr": true, }, @@ -100,18 +98,12 @@ "resources": { "enabled": true, - "thresholds_conns": [ - {"address": "*internal"} - ], "string_indexed_fields": ["Account"], }, "stats": { "enabled": true, - "thresholds_conns": [ - {"address": "*internal"} - ], "string_indexed_fields": ["Account","RunID","Destination"], }, @@ -125,13 +117,13 @@ "suppliers": { "enabled": true, "rals_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "resources_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "stats_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "string_indexed_fields": ["Account"], }, diff --git a/data/tutorials/fs_evsock/cgrates/etc/cgrates/cgrates.json b/data/tutorials/fs_evsock/cgrates/etc/cgrates/cgrates.json index 60d12d4cb..89506a251 100644 --- a/data/tutorials/fs_evsock/cgrates/etc/cgrates/cgrates.json +++ b/data/tutorials/fs_evsock/cgrates/etc/cgrates/cgrates.json @@ -1,13 +1,11 @@ { -// Real-time Charging System for Telecom & ISP environments +// Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments // Copyright (C) ITsysCOM GmbH -// -// This file contains the default configuration hardcoded into CGRateS. -// This is what you get when you load CGRateS with an empty configuration file. "general": { - "log_level":7, + "log_level": 7, + "node_id":"CGRFreeswitch", }, @@ -31,16 +29,10 @@ "rals": { "enabled": true, "thresholds_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} - ], - "pubsubs_conns": [ - {"address": "*internal"} - ], - "attributes_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], }, @@ -48,10 +40,10 @@ "cdrs": { "enabled": true, "sessions_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "sessions_cost_retries": 5, }, @@ -60,25 +52,25 @@ "sessions": { "enabled": true, "rals_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "cdrs_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "resources_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "suppliers_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "attributes_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "thresholds_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "debit_interval": "5s", "channel_sync_interval":"7s", @@ -87,17 +79,12 @@ "freeswitch_agent": { "enabled": true, - "sessions_conns": [ - {"address": "*internal"} - ], "event_socket_conns":[ {"address": "127.0.0.1:8021", "password": "ClueCon", "reconnects": -1,"alias":""} ], -}, - - -"pubsubs": { - "enabled": true, + "sessions_conns": [ + {"address": "*internal"}, + ], }, @@ -109,18 +96,12 @@ "resources": { "enabled": true, - "thresholds_conns": [ - {"address": "*internal"} - ], "string_indexed_fields": ["Account"], }, "stats": { "enabled": true, - "thresholds_conns": [ - {"address": "*internal"} - ], "string_indexed_fields": ["Account","RunID","Destination"], }, @@ -134,13 +115,13 @@ "suppliers": { "enabled": true, "rals_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "resources_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "stats_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "string_indexed_fields": ["Account"], }, diff --git a/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json b/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json index 47c99d8f6..9a3701cb7 100644 --- a/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json +++ b/data/tutorials/kamevapi/cgrates/etc/cgrates/cgrates.json @@ -29,10 +29,10 @@ "rals": { "enabled": true, "thresholds_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], }, @@ -40,10 +40,10 @@ "cdrs": { "enabled": true, "sessions_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "sessions_cost_retries": 5, }, @@ -52,25 +52,25 @@ "sessions": { "enabled": true, "rals_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "cdrs_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "resources_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "suppliers_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "attributes_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "stats_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "thresholds_conns": [ - {"address": "127.0.0.1:2012", "transport": "*json"} + {"address": "127.0.0.1:2012", "transport": "*json"}, ], "debit_interval": "5s", "channel_sync_interval":"7s", @@ -79,18 +79,13 @@ "kamailio_agent": { "enabled": true, - "evapi_conns":[ // instantiate connections to multiple Kamailio servers + "evapi_conns":[ {"address": "127.0.0.1:8448", "reconnects": 5} ], "sessions_conns": [ - {"address": "*internal"} // connection towards session service: <*internal> + {"address": "*internal"}, ], - "create_cdr": true, // create CDR out of events and sends them to CDRS component -}, - - -"pubsubs": { - "enabled": true, + "create_cdr": true, }, @@ -102,18 +97,12 @@ "resources": { "enabled": true, - "thresholds_conns": [ - {"address": "*internal"} - ], "string_indexed_fields": ["Account"], }, "stats": { "enabled": true, - "thresholds_conns": [ - {"address": "*internal"} - ], "string_indexed_fields": ["Account","RunID","Destination"], }, @@ -127,13 +116,13 @@ "suppliers": { "enabled": true, "rals_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "resources_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "stats_conns": [ - {"address": "*internal"} + {"address": "*internal"}, ], "string_indexed_fields": ["Account"], }, diff --git a/general_tests/tutorial_calls_test.go b/general_tests/tutorial_calls_test.go index 9c524b319..eb5424c3a 100755 --- a/general_tests/tutorial_calls_test.go +++ b/general_tests/tutorial_calls_test.go @@ -529,7 +529,7 @@ func testCall1002Cdrs(t *testing.T) { if reply[0].RequestType != utils.META_POSTPAID { t.Errorf("Unexpected RequestType for CDR: %+v", reply[0].RequestType) } - if reply[0].Usage != "1m5s" { // Usage as seconds + if reply[0].Usage != "1m5s" && reply[0].Usage != "1m6s" { // Usage as seconds t.Errorf("Unexpected Usage for CDR: %+v", reply[0].Usage) } if reply[0].CostSource != utils.MetaCDRs { @@ -552,7 +552,8 @@ func testCall1003Cdrs(t *testing.T) { if cdr.RequestType != utils.META_PREPAID { t.Errorf("Unexpected RequestType for CDR: %+v", cdr.RequestType) } - if cdr.Usage != "15s" && cdr.Usage != "20s" { // Usage as seconds + if cdr.Usage != "15s" && cdr.Usage != "16s" && + cdr.Usage != "20s" && cdr.Usage != "21s" { // Usage as seconds t.Errorf("Unexpected Usage for CDR: %+v", cdr.Usage) } if cdr.CostSource != utils.MetaSessionS { @@ -566,30 +567,40 @@ func testCall1003Cdrs(t *testing.T) { func testCallStatMetrics(t *testing.T) { var metrics map[string]string firstStatMetrics1 := map[string]string{ - utils.MetaTCC: "1.22009", - utils.MetaTCD: "2m12s", + utils.MetaTCC: "1.35346", + utils.MetaTCD: "2m27s", } firstStatMetrics2 := map[string]string{ + utils.MetaTCC: "1.35009", + utils.MetaTCD: "2m25s", + } + firstStatMetrics3 := map[string]string{ utils.MetaTCC: "1.34009", utils.MetaTCD: "2m24s", } - secondStatMetrics := map[string]string{ + secondStatMetrics1 := map[string]string{ utils.MetaTCC: "0.6", utils.MetaTCD: "35s", } + secondStatMetrics2 := map[string]string{ + utils.MetaTCC: "0.6", + utils.MetaTCD: "37s", + } if err := tutorialCallsRpc.Call(utils.StatSv1GetQueueStringMetrics, &utils.TenantID{Tenant: "cgrates.org", ID: "Stats2"}, &metrics); err != nil { t.Error(err) } else if !reflect.DeepEqual(firstStatMetrics1, metrics) && - !reflect.DeepEqual(firstStatMetrics2, metrics) { + !reflect.DeepEqual(firstStatMetrics2, metrics) && + !reflect.DeepEqual(firstStatMetrics3, metrics) { t.Errorf("expecting: %+v, received reply: %s", firstStatMetrics1, metrics) } if err := tutorialCallsRpc.Call(utils.StatSv1GetQueueStringMetrics, &utils.TenantID{Tenant: "cgrates.org", ID: "Stats2_1"}, &metrics); err != nil { t.Error(err) - } else if !reflect.DeepEqual(secondStatMetrics, metrics) { - t.Errorf("expecting: %+v, received reply: %s", secondStatMetrics, metrics) + } else if !reflect.DeepEqual(secondStatMetrics1, metrics) && + !reflect.DeepEqual(secondStatMetrics2, metrics) { + t.Errorf("expecting: %+v, received reply: %s", secondStatMetrics1, metrics) } } From 6a89dd1d6ba3f6662ea091d8f96900cdb59dcac8 Mon Sep 17 00:00:00 2001 From: TeoV Date: Thu, 15 Nov 2018 10:38:57 -0500 Subject: [PATCH 10/12] Update aringo library --- agents/asteriskagent.go | 42 ++++++++++++++++------------ general_tests/tutorial_calls_test.go | 39 ++++++++++++++++++++++---- glide.lock | 2 +- sessions/sessions.go | 2 +- 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/agents/asteriskagent.go b/agents/asteriskagent.go index ccef7ab59..cee52e37a 100644 --- a/agents/asteriskagent.go +++ b/agents/asteriskagent.go @@ -19,6 +19,7 @@ along with this program. If not, see package agents import ( + "encoding/json" "fmt" "net/url" "strconv" @@ -99,6 +100,7 @@ func (sma *AsteriskAgent) ListenAndServe() (err error) { case astRawEv := <-sma.astEvChan: smAsteriskEvent := NewSMAsteriskEvent(astRawEv, strings.Split(sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address, ":")[0]) + switch smAsteriskEvent.EventType() { case ARIStasisStart: go sma.handleStasisStart(smAsteriskEvent) @@ -192,12 +194,13 @@ func (sma *AsteriskAgent) handleStasisStart(ev *SMAsteriskEvent) { if *authReply.MaxUsage == time.Duration(0) { sma.hangupChannel(ev.ChannelID(), "") return - } else if *authReply.MaxUsage != time.Duration(-1) { - // Set absolute timeout for non-postpaid calls - if !sma.setChannelVar(ev.ChannelID(), CGRMaxSessionTime, - strconv.Itoa(int(authReply.MaxUsage.Seconds()*1000))) { - return - } + } else if *authReply.MaxUsage == time.Duration(-1) { + *authReply.MaxUsage = sma.cgrCfg.SessionSCfg().MaxCallDuration + } + // Set absolute timeout for non-postpaid calls + if !sma.setChannelVar(ev.ChannelID(), CGRMaxSessionTime, + strconv.Itoa(int(authReply.MaxUsage.Seconds()*1000))) { + return } } if authReply.ResourceAllocation != nil { @@ -232,6 +235,7 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { if ev.ChannelState() != channelUp { return } + sma.evCacheMux.RLock() cgrEv, hasIt := sma.eventsCache[ev.ChannelID()] sma.evCacheMux.RUnlock() @@ -331,23 +335,25 @@ func (sma *AsteriskAgent) Call(serviceMethod string, args interface{}, reply int } func (sma *AsteriskAgent) V1GetActiveSessionIDs(ignParam string, - sessionIDs *[]*sessions.SessionID) (err error) { - var sIDs []*sessions.SessionID - i := 0 - sma.evCacheMux.RLock() - originIds := make([]string, len(sma.eventsCache)) - for orgId := range sma.eventsCache { - originIds[i] = orgId - i++ + sessionIDs *[]*sessions.SessionID) error { + var slMpIface []map[string]interface{} // decode the result from ari into a slice of map[string]interface{} + if mp, err := sma.astConn.Call( + aringo.HTTP_GET, + fmt.Sprintf("http://%s/ari/channels", + sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address), + nil); err != nil { + return err + } else if err := json.Unmarshal(mp, &slMpIface); err != nil { + return err } - sma.evCacheMux.RUnlock() - for _, orgId := range originIds { + var sIDs []*sessions.SessionID + for _, mpIface := range slMpIface { sIDs = append(sIDs, &sessions.SessionID{ OriginHost: strings.Split(sma.cgrCfg.AsteriskAgentCfg().AsteriskConns[sma.astConnIdx].Address, ":")[0], - OriginID: orgId}, + OriginID: mpIface["id"].(string)}, ) } *sessionIDs = sIDs - return + return nil } diff --git a/general_tests/tutorial_calls_test.go b/general_tests/tutorial_calls_test.go index eb5424c3a..55c0ad396 100755 --- a/general_tests/tutorial_calls_test.go +++ b/general_tests/tutorial_calls_test.go @@ -27,6 +27,7 @@ import ( "os" "path" "reflect" + "strings" "testing" "time" @@ -498,6 +499,10 @@ func testCall1001Cdrs(t *testing.T) { t.Errorf("Unexpected RequestType for CDR: %+v", cdr.RequestType) } if cdr.Destination == "1002" { + // in case of Asterisk take the integer part from usage + if optConf == utils.Asterisk { + cdr.Usage = strings.Split(cdr.Usage, ".")[0] + "s" + } if cdr.Usage != "1m7s" && cdr.Usage != "1m8s" { // Usage as seconds t.Errorf("Unexpected Usage for CDR: %+v", cdr.Usage) } @@ -505,6 +510,10 @@ func testCall1001Cdrs(t *testing.T) { t.Errorf("Unexpected CostSource for CDR: %+v", cdr.CostSource) } } else if cdr.Destination == "1003" { + // in case of Asterisk take the integer part from usage + if optConf == utils.Asterisk { + cdr.Usage = strings.Split(cdr.Usage, ".")[0] + "s" + } if cdr.Usage != "12s" && cdr.Usage != "13s" { // Usage as seconds t.Errorf("Unexpected Usage for CDR: %+v", cdr.Usage) } @@ -529,6 +538,10 @@ func testCall1002Cdrs(t *testing.T) { if reply[0].RequestType != utils.META_POSTPAID { t.Errorf("Unexpected RequestType for CDR: %+v", reply[0].RequestType) } + // in case of Asterisk take the integer part from usage + if optConf == utils.Asterisk { + reply[0].Usage = strings.Split(reply[0].Usage, ".")[0] + "s" + } if reply[0].Usage != "1m5s" && reply[0].Usage != "1m6s" { // Usage as seconds t.Errorf("Unexpected Usage for CDR: %+v", reply[0].Usage) } @@ -552,6 +565,10 @@ func testCall1003Cdrs(t *testing.T) { if cdr.RequestType != utils.META_PREPAID { t.Errorf("Unexpected RequestType for CDR: %+v", cdr.RequestType) } + // in case of Asterisk take the integer part from usage + if optConf == utils.Asterisk { + cdr.Usage = strings.Split(cdr.Usage, ".")[0] + "s" + } if cdr.Usage != "15s" && cdr.Usage != "16s" && cdr.Usage != "20s" && cdr.Usage != "21s" { // Usage as seconds t.Errorf("Unexpected Usage for CDR: %+v", cdr.Usage) @@ -578,6 +595,10 @@ func testCallStatMetrics(t *testing.T) { utils.MetaTCC: "1.34009", utils.MetaTCD: "2m24s", } + firstStatMetrics4 := map[string]string{ + utils.MetaTCC: "1.35346", + utils.MetaTCD: "2m24s", + } secondStatMetrics1 := map[string]string{ utils.MetaTCC: "0.6", utils.MetaTCD: "35s", @@ -590,15 +611,24 @@ func testCallStatMetrics(t *testing.T) { if err := tutorialCallsRpc.Call(utils.StatSv1GetQueueStringMetrics, &utils.TenantID{Tenant: "cgrates.org", ID: "Stats2"}, &metrics); err != nil { t.Error(err) - } else if !reflect.DeepEqual(firstStatMetrics1, metrics) && + } + if optConf == utils.Asterisk { + metrics[utils.MetaTCD] = strings.Split(metrics[utils.MetaTCD], ".")[0] + "s" + } + if !reflect.DeepEqual(firstStatMetrics1, metrics) && !reflect.DeepEqual(firstStatMetrics2, metrics) && - !reflect.DeepEqual(firstStatMetrics3, metrics) { + !reflect.DeepEqual(firstStatMetrics3, metrics) && + !reflect.DeepEqual(firstStatMetrics4, metrics) { t.Errorf("expecting: %+v, received reply: %s", firstStatMetrics1, metrics) } if err := tutorialCallsRpc.Call(utils.StatSv1GetQueueStringMetrics, &utils.TenantID{Tenant: "cgrates.org", ID: "Stats2_1"}, &metrics); err != nil { t.Error(err) - } else if !reflect.DeepEqual(secondStatMetrics1, metrics) && + } + if optConf == utils.Asterisk { + metrics[utils.MetaTCD] = strings.Split(metrics[utils.MetaTCD], ".")[0] + "s" + } + if !reflect.DeepEqual(secondStatMetrics1, metrics) && !reflect.DeepEqual(secondStatMetrics2, metrics) { t.Errorf("expecting: %+v, received reply: %s", secondStatMetrics1, metrics) } @@ -772,8 +802,7 @@ func testCallSyncSessions(t *testing.T) { t.Errorf("Resources: %+v", rsAfter) } for _, r := range *rsAfter { - if r.ID == "ResGroup1" && - (len(r.Usages) != 0 || len(r.TTLIdx) != 0) { + if r.ID == "ResGroup1" && len(r.Usages) != 0 { t.Errorf("Unexpected resource: %+v", utils.ToJSON(r)) } } diff --git a/glide.lock b/glide.lock index 1fbb5c00e..c0f2f3d18 100644 --- a/glide.lock +++ b/glide.lock @@ -8,7 +8,7 @@ imports: subpackages: - jsonrpc - name: github.com/cgrates/aringo - version: 47cdb110c5ff42bddf2b801dc5ae8ceb15d2d602 + version: f996da7890eaec95ba13240253744446e17e6598 - name: github.com/cgrates/fsock version: bcbd5e75c07dddb12ac86f1f861f2bdddc1d4596 - name: github.com/cgrates/kamevapi diff --git a/sessions/sessions.go b/sessions/sessions.go index 15643a6df..a2271ee83 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -292,7 +292,7 @@ func (smg *SMGeneric) ttlTerminate(s *SMGSession, tmtr *smgSessionTerminator) { var reply string argsRU := utils.ArgRSv1ResourceUsage{ CGREvent: utils.CGREvent{ - Tenant: s.EventStart.GetStringIgnoreErrors(utils.Tenant), + Tenant: s.Tenant, Event: s.EventStart.AsMapInterface(), }, UsageID: s.ResourceID, From 39f1c2f1eed5301ac926bb704ca904c018b21bd0 Mon Sep 17 00:00:00 2001 From: TeoV Date: Thu, 15 Nov 2018 10:51:54 -0500 Subject: [PATCH 11/12] Update freeswitch_conf.tar.gz --- .../freeswitch/etc/freeswitch_conf.tar.gz | Bin 27865 -> 27632 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/tutorials/fs_evsock/freeswitch/etc/freeswitch_conf.tar.gz b/data/tutorials/fs_evsock/freeswitch/etc/freeswitch_conf.tar.gz index a40e883a3d143e92fc4ffb4c91b0f963ec907d93..28ba3a0fb5b71c83cb300ec6632638d927bcb727 100644 GIT binary patch literal 27632 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 g!SIjsXk^0vFE_h`QUCV_3MW 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 From daa699f79eb0e17fd0b285b3baa63621546e3196 Mon Sep 17 00:00:00 2001 From: TeoV Date: Thu, 15 Nov 2018 10:54:32 -0500 Subject: [PATCH 12/12] Remove empty space --- agents/asteriskagent.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/agents/asteriskagent.go b/agents/asteriskagent.go index cee52e37a..768e2838c 100644 --- a/agents/asteriskagent.go +++ b/agents/asteriskagent.go @@ -235,14 +235,12 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { if ev.ChannelState() != channelUp { return } - sma.evCacheMux.RLock() cgrEv, hasIt := sma.eventsCache[ev.ChannelID()] sma.evCacheMux.RUnlock() if !hasIt { // Not handled by us return } - sma.evCacheMux.Lock() err := ev.UpdateCGREvent(cgrEv) // Updates the event directly in the cache sma.evCacheMux.Unlock() @@ -252,10 +250,8 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { utils.AsteriskAgent, err.Error(), ev.ChannelID())) return } - // populate init session args initSessionArgs := ev.V1InitSessionArgs(*cgrEv) - if initSessionArgs == nil { utils.Logger.Err(fmt.Sprintf("<%s> event: %s cannot generate init session arguments", utils.AsteriskAgent, ev.ChannelID())) @@ -274,7 +270,6 @@ func (sma *AsteriskAgent) handleChannelStateChange(ev *SMAsteriskEvent) { sma.hangupChannel(ev.ChannelID(), "") return } - } // Channel disconnect @@ -301,6 +296,7 @@ func (sma *AsteriskAgent) handleChannelDestroyed(ev *SMAsteriskEvent) { utils.AsteriskAgent, ev.ChannelID())) return } + var reply string if err := sma.smg.Call(utils.SessionSv1TerminateSession, tsArgs, &reply); err != nil {