mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Compare commits
273 Commits
v0.9.1-rc3
...
v0.9.1rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24fda5b14b | ||
|
|
f3f6bb1e16 | ||
|
|
9211c01d69 | ||
|
|
87481ed520 | ||
|
|
5857e63823 | ||
|
|
9ebf2573b0 | ||
|
|
e67db4a434 | ||
|
|
375bf8c0dd | ||
|
|
6acfa22a04 | ||
|
|
29e3bd137b | ||
|
|
f390281f41 | ||
|
|
881db9c1c4 | ||
|
|
def252d153 | ||
|
|
2bc4e3fb7e | ||
|
|
e381265ace | ||
|
|
15f2a2cd82 | ||
|
|
4bb3e745f1 | ||
|
|
4045d022ae | ||
|
|
5e06cafd2f | ||
|
|
a93ecfc049 | ||
|
|
5d0e6b4fee | ||
|
|
5de1a83bf5 | ||
|
|
c6de153685 | ||
|
|
22adfb552f | ||
|
|
b0cd66509c | ||
|
|
4d02ede7df | ||
|
|
d77c5463ac | ||
|
|
19b7d0beb7 | ||
|
|
315eddb63f | ||
|
|
acda24e46e | ||
|
|
af16503069 | ||
|
|
822a921a37 | ||
|
|
6017827059 | ||
|
|
3892d9ed20 | ||
|
|
5322063103 | ||
|
|
e8393b9bcc | ||
|
|
724ebdc039 | ||
|
|
2dbc80166e | ||
|
|
ccac8236f4 | ||
|
|
3c34310ab2 | ||
|
|
2ef0f4f492 | ||
|
|
f6741c6d88 | ||
|
|
62e9d02239 | ||
|
|
2e80f8a1f4 | ||
|
|
3aecec9bb8 | ||
|
|
e2e3b8d2f2 | ||
|
|
86317e4b19 | ||
|
|
f2f46f8054 | ||
|
|
c66f4f2710 | ||
|
|
ee31976401 | ||
|
|
f6d16cecc5 | ||
|
|
db433a760f | ||
|
|
47700a924f | ||
|
|
548a4ea64f | ||
|
|
3227434fd9 | ||
|
|
c344554bbb | ||
|
|
57be22dd87 | ||
|
|
a3cc1fedfd | ||
|
|
6685b7b444 | ||
|
|
10b27fa978 | ||
|
|
e2a50a77fd | ||
|
|
00b47c1367 | ||
|
|
cdd83ea531 | ||
|
|
d33299f0ab | ||
|
|
5083c7479d | ||
|
|
37b21d2bb5 | ||
|
|
e09cc8527e | ||
|
|
0855d09fea | ||
|
|
c8553e9611 | ||
|
|
d8fb33aee3 | ||
|
|
3f30cdb7ed | ||
|
|
b9681f2d1c | ||
|
|
d6a0825142 | ||
|
|
8d61099de1 | ||
|
|
6ba20aee03 | ||
|
|
b28a02512c | ||
|
|
5ae7a18283 | ||
|
|
5b10f63c94 | ||
|
|
90e3791c0d | ||
|
|
fdeecafe37 | ||
|
|
71a50dc78e | ||
|
|
8f47264248 | ||
|
|
a753a55119 | ||
|
|
2556fb5e69 | ||
|
|
0b0474fa23 | ||
|
|
8967c2fa3a | ||
|
|
bea8434d99 | ||
|
|
bd5b2607e8 | ||
|
|
62ccb5b9df | ||
|
|
755ab02d55 | ||
|
|
cf0179e505 | ||
|
|
bde488c46b | ||
|
|
68aefe4912 | ||
|
|
487a1c5da6 | ||
|
|
64711c6b87 | ||
|
|
781db890d1 | ||
|
|
7b618f8806 | ||
|
|
9006c5ed18 | ||
|
|
e6b02b84c0 | ||
|
|
b557e2a5a4 | ||
|
|
9a06d5e537 | ||
|
|
be8ea3bcc9 | ||
|
|
4833eb94c3 | ||
|
|
f717bb9542 | ||
|
|
2646e8fa6e | ||
|
|
ed313c3222 | ||
|
|
25b15b8f0f | ||
|
|
7d9326965d | ||
|
|
52ae44fc34 | ||
|
|
a0aa0cc05d | ||
|
|
b9d52c6401 | ||
|
|
48d1720430 | ||
|
|
39e93e7fb8 | ||
|
|
49a3df285b | ||
|
|
fc91e35433 | ||
|
|
95d4af8ab7 | ||
|
|
83dd4efeab | ||
|
|
b3dee97bc4 | ||
|
|
074313b0f8 | ||
|
|
8d98436656 | ||
|
|
477af9467f | ||
|
|
1a8e7f4631 | ||
|
|
670b748b01 | ||
|
|
ddbd97ad20 | ||
|
|
fe4506008f | ||
|
|
0aa5223f78 | ||
|
|
a37dbf0734 | ||
|
|
7831a53797 | ||
|
|
f701a42948 | ||
|
|
164b9f8945 | ||
|
|
a3daecdbc9 | ||
|
|
a91873800d | ||
|
|
f463dd2755 | ||
|
|
f052b14214 | ||
|
|
5f1923d694 | ||
|
|
c52b161b73 | ||
|
|
d7a0446585 | ||
|
|
216ae2b767 | ||
|
|
e60fc8cb96 | ||
|
|
8008c76154 | ||
|
|
d929e14159 | ||
|
|
dd2eb2f97a | ||
|
|
e0476a3130 | ||
|
|
fdc451244a | ||
|
|
098a1f6d3f | ||
|
|
75046b1f36 | ||
|
|
dec4ebf2f1 | ||
|
|
4154a3beef | ||
|
|
0efb2f1094 | ||
|
|
5371688158 | ||
|
|
51ee4091bc | ||
|
|
540412033f | ||
|
|
a731a33238 | ||
|
|
96092faffc | ||
|
|
beea40977c | ||
|
|
ffaeac112c | ||
|
|
9c46b61c73 | ||
|
|
1ab852e3cd | ||
|
|
4e956eaa8c | ||
|
|
a5f5274095 | ||
|
|
4c894ee9de | ||
|
|
361d75d8cf | ||
|
|
64e79764de | ||
|
|
6ad5794bfc | ||
|
|
c447fa49dc | ||
|
|
9042c98492 | ||
|
|
bd71dc9a4c | ||
|
|
944262ccff | ||
|
|
fc1e7aed5a | ||
|
|
b91fc4ca21 | ||
|
|
b784edf5aa | ||
|
|
914fe77117 | ||
|
|
77ea2754cb | ||
|
|
cd531849d1 | ||
|
|
dadf06f3ef | ||
|
|
9e88719e05 | ||
|
|
3a3b92e9d6 | ||
|
|
a9d698e029 | ||
|
|
aa99a1e526 | ||
|
|
f154dac933 | ||
|
|
59f650e3f8 | ||
|
|
462152a76d | ||
|
|
3bf3f04cc6 | ||
|
|
9f9174d0cc | ||
|
|
e6ef9f8155 | ||
|
|
bea20cf98d | ||
|
|
59e6e945b5 | ||
|
|
77c326cccf | ||
|
|
709594ffa9 | ||
|
|
58d485f46f | ||
|
|
4cbadb4158 | ||
|
|
caba3af732 | ||
|
|
727337e617 | ||
|
|
20a0ae4449 | ||
|
|
5f331f3161 | ||
|
|
c7ea2cd263 | ||
|
|
30a842a6b9 | ||
|
|
3f7bc14b44 | ||
|
|
6fcd794005 | ||
|
|
750260505c | ||
|
|
5c2d1a9c1a | ||
|
|
d90e67b966 | ||
|
|
0b7f315d49 | ||
|
|
a239b8d760 | ||
|
|
0490eaef8c | ||
|
|
ea6c8371d2 | ||
|
|
4032274748 | ||
|
|
edf5007f9e | ||
|
|
31eebb7e81 | ||
|
|
5d190f0f2a | ||
|
|
a5fad89574 | ||
|
|
23a676da95 | ||
|
|
69cf1cc896 | ||
|
|
b735688fe5 | ||
|
|
6a251c2b2c | ||
|
|
21b4c10836 | ||
|
|
1bbe5a0a88 | ||
|
|
8733b5219e | ||
|
|
f61aca8d91 | ||
|
|
3bbb67c72f | ||
|
|
8177e64f8b | ||
|
|
9210b20924 | ||
|
|
5b424d0e70 | ||
|
|
b6dde967f2 | ||
|
|
c50c202fe6 | ||
|
|
5f9d18fe0f | ||
|
|
a31231de09 | ||
|
|
ddeb13bcc4 | ||
|
|
70d0e0171c | ||
|
|
ba3cabb7bb | ||
|
|
5d857f1255 | ||
|
|
4f3e91d8ca | ||
|
|
7c52e1e692 | ||
|
|
81cc68e0c5 | ||
|
|
f51e0b10e3 | ||
|
|
999ac0aead | ||
|
|
02297d4d36 | ||
|
|
95fde288ad | ||
|
|
ea75cd3aa2 | ||
|
|
21875f7ed7 | ||
|
|
7697b74713 | ||
|
|
46b87953da | ||
|
|
5e72e1c528 | ||
|
|
baf590dab3 | ||
|
|
2c3e8e1584 | ||
|
|
962cf17884 | ||
|
|
ac60a39852 | ||
|
|
0005ba2f68 | ||
|
|
08e5d8540a | ||
|
|
6c96937059 | ||
|
|
080395e7ea | ||
|
|
2896053199 | ||
|
|
0348be416c | ||
|
|
13707ca377 | ||
|
|
8ec403f151 | ||
|
|
abd0a00ecb | ||
|
|
e97b72a30a | ||
|
|
ea6337d9fa | ||
|
|
2a1f6ff583 | ||
|
|
b7cdf53858 | ||
|
|
99e269b894 | ||
|
|
c50a61fec5 | ||
|
|
d765e0090b | ||
|
|
d776219013 | ||
|
|
7bc182e374 | ||
|
|
fb1c7265f5 | ||
|
|
ca6957cb48 | ||
|
|
1471c47bda | ||
|
|
c8fb090c11 | ||
|
|
1c2d2116fd | ||
|
|
3f16c1c12d | ||
|
|
9854ce7068 | ||
|
|
58ddae9b70 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,3 +10,5 @@ docs/_*
|
||||
bin
|
||||
.idea
|
||||
dean*
|
||||
data/vagrant/.vagrant
|
||||
data/vagrant/vagrant_ansible_inventory_default
|
||||
|
||||
@@ -15,6 +15,6 @@ notifications:
|
||||
on_success: change
|
||||
on_failure: always
|
||||
email:
|
||||
on_success: change
|
||||
on_success: never
|
||||
on_failure: always
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -1,5 +1,7 @@
|
||||
## Rating system for Telecom & ISP environments ##
|
||||
|
||||
[](https://drone.io/github.com/cgrates/cgrates/latest) [](http://travis-ci.org/cgrates/cgrates)
|
||||
|
||||
### Features ###
|
||||
+ Rates for prepaid and for postpaid
|
||||
+ The budget expressed in money and/or minutes (seconds)
|
||||
@@ -12,14 +14,14 @@
|
||||
+ Commercial support available
|
||||
|
||||
### Documentation ###
|
||||
Install & run screencast: http://youtu.be/qTQZZpb-m7Q
|
||||
[Step by steps tutorials](https://cgrates.readthedocs.org/en/latest/tut_freeswitch.html)
|
||||
|
||||
Browsable HTML http://readthedocs.org/docs/cgrates/
|
||||
[Debian apt-get repository](https://cgrates.readthedocs.org/en/latest/tut_freeswitch_installs.html#cgrates)
|
||||
|
||||
Browsable HTML docs http://readthedocs.org/docs/cgrates/
|
||||
|
||||
PDF, Epub, Manpage http://readthedocs.org/projects/cgrates/downloads/
|
||||
|
||||
API reference [godoc](http://godoc.org/github.com/cgrates/cgrates) or [gowalker](http://gowalker.org/github.com/cgrates/cgrates)
|
||||
API reference [godoc](http://godoc.org/github.com/cgrates/cgrates/apier)
|
||||
|
||||
Also check irc.freenode.net#cgrates and [Google group](https://groups.google.com/forum/#!forum/cgrates) for a more real-time support.
|
||||
|
||||
[](https://drone.io/github.com/cgrates/cgrates/latest) [](http://travis-ci.org/cgrates/cgrates) [](https://bitdeli.com/free "Bitdeli Badge")
|
||||
|
||||
@@ -21,9 +21,10 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
type AttrAcntAction struct {
|
||||
@@ -33,13 +34,13 @@ type AttrAcntAction struct {
|
||||
}
|
||||
|
||||
type AccountActionTiming struct {
|
||||
Id string // The id to reference this particular ActionTiming
|
||||
ActionTimingsId string // The id of the ActionTimings profile attached to the account
|
||||
ActionsId string // The id of actions which will be executed
|
||||
NextExecTime time.Time // Next execution time
|
||||
ActionPlanId string // The id of the ActionPlanId profile attached to the account
|
||||
Uuid string // The id to reference this particular ActionTiming
|
||||
ActionsId string // The id of actions which will be executed
|
||||
NextExecTime time.Time // Next execution time
|
||||
}
|
||||
|
||||
func (self *ApierV1) GetAccountActionTimings(attrs AttrAcntAction, reply *[]*AccountActionTiming) error {
|
||||
func (self *ApierV1) GetAccountActionPlan(attrs AttrAcntAction, reply *[]*AccountActionTiming) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"Tenant", "Account", "Direction"}); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
@@ -50,8 +51,8 @@ func (self *ApierV1) GetAccountActionTimings(attrs AttrAcntAction, reply *[]*Acc
|
||||
}
|
||||
for _, ats := range allATs {
|
||||
for _, at := range ats {
|
||||
if utils.IsSliceMember(at.UserBalanceIds, utils.BalanceKey(attrs.Tenant, attrs.Account, attrs.Direction)) {
|
||||
accountATs = append(accountATs, &AccountActionTiming{Id: at.Id, ActionTimingsId: at.Tag, ActionsId: at.ActionsId, NextExecTime: at.GetNextStartTime()})
|
||||
if utils.IsSliceMember(at.AccountIds, utils.BalanceKey(attrs.Tenant, attrs.Account, attrs.Direction)) {
|
||||
accountATs = append(accountATs, &AccountActionTiming{Uuid: at.Uuid, ActionPlanId: at.Id, ActionsId: at.ActionsId, NextExecTime: at.GetNextStartTime(time.Now())})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,17 +61,17 @@ func (self *ApierV1) GetAccountActionTimings(attrs AttrAcntAction, reply *[]*Acc
|
||||
}
|
||||
|
||||
type AttrRemActionTiming struct {
|
||||
ActionTimingsId string // Id identifying the ActionTimings profile
|
||||
ActionPlanId string // Id identifying the ActionTimings profile
|
||||
ActionTimingId string // Internal CGR id identifying particular ActionTiming, *all for all user related ActionTimings to be canceled
|
||||
Tenant string // Tenant he account belongs to
|
||||
Account string // Account name
|
||||
Direction string // Traffic direction
|
||||
ReloadScheduler bool // If set it will reload the scheduler after adding
|
||||
ReloadScheduler bool // If set it will reload the scheduler after adding
|
||||
}
|
||||
|
||||
// Removes an ActionTimings or parts of it depending on filters being set
|
||||
func (self *ApierV1) RemActionTiming(attrs AttrRemActionTiming, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"ActionTimingsId"}); len(missing) != 0 { // Only mandatory ActionTimingsId
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"ActionPlanId"}); len(missing) != 0 { // Only mandatory ActionPlanId
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if len(attrs.Account) != 0 { // Presence of Account requires complete account details to be provided
|
||||
@@ -79,14 +80,14 @@ func (self *ApierV1) RemActionTiming(attrs AttrRemActionTiming, reply *string) e
|
||||
}
|
||||
}
|
||||
_, err := engine.AccLock.Guard(engine.ACTION_TIMING_PREFIX, func() (float64, error) {
|
||||
ats, err := self.AccountDb.GetActionTimings(attrs.ActionTimingsId)
|
||||
ats, err := self.AccountDb.GetActionTimings(attrs.ActionPlanId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if len(ats) == 0 {
|
||||
return 0, errors.New(utils.ERR_NOT_FOUND)
|
||||
}
|
||||
ats = engine.RemActionTiming(ats, attrs.ActionTimingId, utils.BalanceKey(attrs.Tenant, attrs.Account, attrs.Direction))
|
||||
if err := self.AccountDb.SetActionTimings(attrs.ActionTimingsId, ats); err != nil {
|
||||
if err := self.AccountDb.SetActionTimings(attrs.ActionPlanId, ats); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
@@ -107,7 +108,7 @@ func (self *ApierV1) GetAccountActionTriggers(attrs AttrAcntAction, reply *engin
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"Tenant", "Account", "Direction"}); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if balance, err := self.AccountDb.GetUserBalance(utils.BalanceKey(attrs.Tenant, attrs.Account, attrs.Direction)); err != nil {
|
||||
if balance, err := self.AccountDb.GetAccount(utils.BalanceKey(attrs.Tenant, attrs.Account, attrs.Direction)); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else {
|
||||
*reply = balance.ActionTriggers
|
||||
@@ -129,7 +130,7 @@ func (self *ApierV1) RemAccountActionTriggers(attrs AttrRemAcntActionTriggers, r
|
||||
}
|
||||
balanceId := utils.BalanceKey(attrs.Tenant, attrs.Account, attrs.Direction)
|
||||
_, err := engine.AccLock.Guard(balanceId, func() (float64, error) {
|
||||
ub, err := self.AccountDb.GetUserBalance(balanceId)
|
||||
ub, err := self.AccountDb.GetAccount(balanceId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -143,7 +144,7 @@ func (self *ApierV1) RemAccountActionTriggers(attrs AttrRemAcntActionTriggers, r
|
||||
ub.ActionTriggers = make(engine.ActionTriggerPriotityList, 0)
|
||||
}
|
||||
}
|
||||
if err := self.AccountDb.SetUserBalance(ub); err != nil {
|
||||
if err := self.AccountDb.SetAccount(ub); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
@@ -155,13 +156,12 @@ func (self *ApierV1) RemAccountActionTriggers(attrs AttrRemAcntActionTriggers, r
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
type AttrSetAccount struct {
|
||||
Tenant string
|
||||
Direction string
|
||||
Account string
|
||||
Type string // <*prepaid|*postpaid>
|
||||
ActionTimingsId string
|
||||
Tenant string
|
||||
Direction string
|
||||
Account string
|
||||
ActionPlanId string
|
||||
AllowNegative bool
|
||||
}
|
||||
|
||||
// Ads a new account into dataDb. If already defined, returns success.
|
||||
@@ -170,35 +170,30 @@ func (self *ApierV1) SetAccount(attr AttrSetAccount, reply *string) error {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
balanceId := utils.BalanceKey(attr.Tenant, attr.Account, attr.Direction)
|
||||
var ub *engine.UserBalance
|
||||
var ats engine.ActionTimings
|
||||
var ub *engine.Account
|
||||
var ats engine.ActionPlan
|
||||
_, err := engine.AccLock.Guard(balanceId, func() (float64, error) {
|
||||
if bal, _ := self.AccountDb.GetUserBalance(balanceId); bal != nil {
|
||||
if bal, _ := self.AccountDb.GetAccount(balanceId); bal != nil {
|
||||
ub = bal
|
||||
} else { // Not found in db, create it here
|
||||
if len(attr.Type) == 0 {
|
||||
attr.Type = engine.UB_TYPE_PREPAID
|
||||
} else if !utils.IsSliceMember([]string{engine.UB_TYPE_POSTPAID, engine.UB_TYPE_PREPAID}, attr.Type) {
|
||||
return 0, fmt.Errorf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, "Type")
|
||||
}
|
||||
ub = &engine.UserBalance{
|
||||
Id: balanceId,
|
||||
Type: attr.Type,
|
||||
ub = &engine.Account{
|
||||
Id: balanceId,
|
||||
AllowNegative: attr.AllowNegative,
|
||||
}
|
||||
}
|
||||
|
||||
if len(attr.ActionTimingsId) != 0 {
|
||||
|
||||
if len(attr.ActionPlanId) != 0 {
|
||||
var err error
|
||||
ats, err = self.AccountDb.GetActionTimings(attr.ActionTimingsId)
|
||||
ats, err = self.AccountDb.GetActionTimings(attr.ActionPlanId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, at := range ats {
|
||||
at.UserBalanceIds = append(at.UserBalanceIds, balanceId)
|
||||
at.AccountIds = append(at.AccountIds, balanceId)
|
||||
}
|
||||
}
|
||||
// All prepared, save account
|
||||
if err := self.AccountDb.SetUserBalance(ub); err != nil {
|
||||
if err := self.AccountDb.SetAccount(ub); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
@@ -208,7 +203,7 @@ func (self *ApierV1) SetAccount(attr AttrSetAccount, reply *string) error {
|
||||
}
|
||||
if len(ats) != 0 {
|
||||
_, err := engine.AccLock.Guard(engine.ACTION_TIMING_PREFIX, func() (float64, error) { // ToDo: Try locking it above on read somehow
|
||||
if err := self.AccountDb.SetActionTimings(attr.ActionTimingsId, ats); err != nil {
|
||||
if err := self.AccountDb.SetActionTimings(attr.ActionPlanId, ats); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
@@ -21,12 +21,14 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/cache2go"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/scheduler"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,6 +40,7 @@ type ApierV1 struct {
|
||||
RatingDb engine.RatingStorage
|
||||
AccountDb engine.AccountingStorage
|
||||
CdrDb engine.CdrStorage
|
||||
LogDb engine.LogStorage
|
||||
Sched *scheduler.Scheduler
|
||||
Config *config.CGRConfig
|
||||
}
|
||||
@@ -60,57 +63,52 @@ func (self *ApierV1) GetRatingPlan(rplnId string, reply *engine.RatingPlan) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
type AttrGetBalance struct {
|
||||
Tenant string
|
||||
Account string
|
||||
BalanceId string
|
||||
Direction string
|
||||
type AttrGetAccount struct {
|
||||
Tenant string
|
||||
Account string
|
||||
BalanceType string
|
||||
Direction string
|
||||
}
|
||||
|
||||
// Get balance
|
||||
func (self *ApierV1) GetBalance(attr *AttrGetBalance, reply *float64) error {
|
||||
func (self *ApierV1) GetAccount(attr *AttrGetAccount, reply *engine.Account) error {
|
||||
tag := fmt.Sprintf("%s:%s:%s", attr.Direction, attr.Tenant, attr.Account)
|
||||
userBalance, err := self.AccountDb.GetUserBalance(tag)
|
||||
userBalance, err := self.AccountDb.GetAccount(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if attr.Direction == "" {
|
||||
attr.Direction = engine.OUTBOUND
|
||||
}
|
||||
|
||||
if balance, balExists := userBalance.BalanceMap[attr.BalanceId+attr.Direction]; !balExists {
|
||||
*reply = 0.0
|
||||
} else {
|
||||
*reply = balance.GetTotalValue()
|
||||
}
|
||||
*reply = *userBalance
|
||||
return nil
|
||||
}
|
||||
|
||||
type AttrAddBalance struct {
|
||||
Tenant string
|
||||
Account string
|
||||
BalanceId string
|
||||
Direction string
|
||||
Value float64
|
||||
Weight float64
|
||||
Overwrite bool // When true it will reset if the balance is already there
|
||||
Tenant string
|
||||
Account string
|
||||
BalanceType string
|
||||
Direction string
|
||||
Value float64
|
||||
ExpirationDate time.Time
|
||||
RatingSubject string
|
||||
DestinationId string
|
||||
Weight float64
|
||||
Overwrite bool // When true it will reset if the balance is already there
|
||||
}
|
||||
|
||||
func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error {
|
||||
tag := fmt.Sprintf("%s:%s:%s", attr.Direction, attr.Tenant, attr.Account)
|
||||
if _, err := self.AccountDb.GetUserBalance(tag); err != nil {
|
||||
if _, err := self.AccountDb.GetAccount(tag); err != nil {
|
||||
// create user balance if not exists
|
||||
ub := &engine.UserBalance{
|
||||
ub := &engine.Account{
|
||||
Id: tag,
|
||||
}
|
||||
if err := self.AccountDb.SetUserBalance(ub); err != nil {
|
||||
if err := self.AccountDb.SetAccount(ub); err != nil {
|
||||
*reply = err.Error()
|
||||
return err
|
||||
}
|
||||
}
|
||||
at := &engine.ActionTiming{
|
||||
UserBalanceIds: []string{tag},
|
||||
AccountIds: []string{tag},
|
||||
}
|
||||
|
||||
if attr.Direction == "" {
|
||||
@@ -120,8 +118,20 @@ func (self *ApierV1) AddBalance(attr *AttrAddBalance, reply *string) error {
|
||||
if attr.Overwrite {
|
||||
aType = engine.TOPUP_RESET
|
||||
}
|
||||
at.SetActions(engine.Actions{&engine.Action{ActionType: aType, BalanceId: attr.BalanceId, Direction: attr.Direction,
|
||||
Balance: &engine.Balance{Value: attr.Value, Weight: attr.Weight}}})
|
||||
at.SetActions(engine.Actions{
|
||||
&engine.Action{
|
||||
ActionType: aType,
|
||||
BalanceType: attr.BalanceType,
|
||||
Direction: attr.Direction,
|
||||
Balance: &engine.Balance{
|
||||
Value: attr.Value,
|
||||
ExpirationDate: attr.ExpirationDate,
|
||||
RateSubject: attr.RatingSubject,
|
||||
DestinationId: attr.DestinationId,
|
||||
Weight: attr.Weight,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := at.Execute(); err != nil {
|
||||
*reply = err.Error()
|
||||
return err
|
||||
@@ -140,8 +150,8 @@ type AttrExecuteAction struct {
|
||||
func (self *ApierV1) ExecuteAction(attr *AttrExecuteAction, reply *string) error {
|
||||
tag := fmt.Sprintf("%s:%s:%s", attr.Direction, attr.Tenant, attr.Account)
|
||||
at := &engine.ActionTiming{
|
||||
UserBalanceIds: []string{tag},
|
||||
ActionsId: attr.ActionsId,
|
||||
AccountIds: []string{tag},
|
||||
ActionsId: attr.ActionsId,
|
||||
}
|
||||
|
||||
if err := at.Execute(); err != nil {
|
||||
@@ -168,6 +178,11 @@ func (self *ApierV1) LoadRatingPlan(attrs AttrLoadRatingPlan, reply *string) err
|
||||
} else if !loaded {
|
||||
return errors.New("NOT_FOUND")
|
||||
}
|
||||
//Automatic cache of the newly inserted rating plan
|
||||
didNotChange := []string{}
|
||||
if err := self.RatingDb.CacheRating(nil, nil, didNotChange, didNotChange); err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = OK
|
||||
return nil
|
||||
}
|
||||
@@ -181,6 +196,11 @@ func (self *ApierV1) LoadRatingProfile(attrs utils.TPRatingProfile, reply *strin
|
||||
if err := dbReader.LoadRatingProfileFiltered(&attrs); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
//Automatic cache of the newly inserted rating profile
|
||||
didNotChange := []string{}
|
||||
if err := self.RatingDb.CacheRating(didNotChange, didNotChange, []string{engine.RATING_PROFILE_PREFIX + attrs.KeyId()}, didNotChange); err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = OK
|
||||
return nil
|
||||
}
|
||||
@@ -191,7 +211,7 @@ type AttrSetRatingProfile struct {
|
||||
Direction string // Traffic direction, OUT is the only one supported for now
|
||||
Subject string // Rating subject, usually the same as account
|
||||
Overwrite bool // Overwrite if exists
|
||||
RatingPlanActivations []*utils.TPRatingActivation // Activate rate profiles at specific time
|
||||
RatingPlanActivations []*utils.TPRatingActivation // Activate rating plans at specific time
|
||||
}
|
||||
|
||||
// Sets a specific rating profile working with data directly in the RatingDb without involving storDb
|
||||
@@ -207,7 +227,7 @@ func (self *ApierV1) SetRatingProfile(attrs AttrSetRatingProfile, reply *string)
|
||||
tpRpf := utils.TPRatingProfile{Tenant: attrs.Tenant, TOR: attrs.TOR, Direction: attrs.Direction, Subject: attrs.Subject}
|
||||
keyId := tpRpf.KeyId()
|
||||
if !attrs.Overwrite {
|
||||
if exists, err := self.RatingDb.ExistsData(engine.RATING_PROFILE_PREFIX, keyId); err != nil {
|
||||
if exists, err := self.RatingDb.HasData(engine.RATING_PROFILE_PREFIX, keyId); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if exists {
|
||||
return errors.New(utils.ERR_EXISTS)
|
||||
@@ -219,7 +239,7 @@ func (self *ApierV1) SetRatingProfile(attrs AttrSetRatingProfile, reply *string)
|
||||
if err != nil {
|
||||
return fmt.Errorf(fmt.Sprintf("%s:Cannot parse activation time from %v", utils.ERR_SERVER_ERROR, ra.ActivationTime))
|
||||
}
|
||||
if exists, err := self.RatingDb.ExistsData(engine.RATING_PLAN_PREFIX, ra.RatingPlanId); err != nil {
|
||||
if exists, err := self.RatingDb.HasData(engine.RATING_PLAN_PREFIX, ra.RatingPlanId); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if !exists {
|
||||
return fmt.Errorf(fmt.Sprintf("%s:RatingPlanId:%s", utils.ERR_NOT_FOUND, ra.RatingPlanId))
|
||||
@@ -230,6 +250,11 @@ func (self *ApierV1) SetRatingProfile(attrs AttrSetRatingProfile, reply *string)
|
||||
if err := self.RatingDb.SetRatingProfile(rpfl); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
//Automatic cache of the newly inserted rating profile
|
||||
didNotChange := []string{}
|
||||
if err := self.RatingDb.CacheRating(didNotChange, didNotChange, []string{engine.RATING_PROFILE_PREFIX + keyId}, didNotChange); err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = OK
|
||||
return nil
|
||||
}
|
||||
@@ -254,7 +279,7 @@ func (self *ApierV1) SetActions(attrs AttrSetActions, reply *string) error {
|
||||
}
|
||||
}
|
||||
if !attrs.Overwrite {
|
||||
if exists, err := self.AccountDb.ExistsData(engine.ACTION_PREFIX, attrs.ActionsId); err != nil {
|
||||
if exists, err := self.AccountDb.HasData(engine.ACTION_PREFIX, attrs.ActionsId); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if exists {
|
||||
return errors.New(utils.ERR_EXISTS)
|
||||
@@ -265,7 +290,7 @@ func (self *ApierV1) SetActions(attrs AttrSetActions, reply *string) error {
|
||||
a := &engine.Action{
|
||||
Id: utils.GenUUID(),
|
||||
ActionType: apiAct.Identifier,
|
||||
BalanceId: apiAct.BalanceType,
|
||||
BalanceType: apiAct.BalanceType,
|
||||
Direction: apiAct.Direction,
|
||||
Weight: apiAct.Weight,
|
||||
ExpirationString: apiAct.ExpiryTime,
|
||||
@@ -287,10 +312,10 @@ func (self *ApierV1) SetActions(attrs AttrSetActions, reply *string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type AttrSetActionTimings struct {
|
||||
ActionTimingsId string // Profile id
|
||||
type AttrSetActionPlan struct {
|
||||
Id string // Profile id
|
||||
ActionPlan []*ApiActionTiming // Set of actions this Actions profile will perform
|
||||
Overwrite bool // If previously defined, will be overwritten
|
||||
ActionTimings []*ApiActionTiming // Set of actions this Actions profile will perform
|
||||
ReloadScheduler bool // Enables automatic reload of the scheduler (eg: useful when adding a single action timing)
|
||||
}
|
||||
|
||||
@@ -304,26 +329,26 @@ type ApiActionTiming struct {
|
||||
Weight float64 // Binding's weight
|
||||
}
|
||||
|
||||
func (self *ApierV1) SetActionTimings(attrs AttrSetActionTimings, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"ActionTimingsId", "ActionTimings"}); len(missing) != 0 {
|
||||
func (self *ApierV1) SetActionPlan(attrs AttrSetActionPlan, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"Id", "ActionPlan"}); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
for _, at := range attrs.ActionTimings {
|
||||
for _, at := range attrs.ActionPlan {
|
||||
requiredFields := []string{"ActionsId", "Time", "Weight"}
|
||||
if missing := utils.MissingStructFields(at, requiredFields); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:Action:%s:%v", utils.ERR_MANDATORY_IE_MISSING, at.ActionsId, missing)
|
||||
}
|
||||
}
|
||||
if !attrs.Overwrite {
|
||||
if exists, err := self.AccountDb.ExistsData(engine.ACTION_TIMING_PREFIX, attrs.ActionTimingsId); err != nil {
|
||||
if exists, err := self.AccountDb.HasData(engine.ACTION_TIMING_PREFIX, attrs.Id); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if exists {
|
||||
return errors.New(utils.ERR_EXISTS)
|
||||
}
|
||||
}
|
||||
storeAtms := make(engine.ActionTimings, len(attrs.ActionTimings))
|
||||
for idx, apiAtm := range attrs.ActionTimings {
|
||||
if exists, err := self.AccountDb.ExistsData(engine.ACTION_PREFIX, apiAtm.ActionsId); err != nil {
|
||||
storeAtms := make(engine.ActionPlan, len(attrs.ActionPlan))
|
||||
for idx, apiAtm := range attrs.ActionPlan {
|
||||
if exists, err := self.AccountDb.HasData(engine.ACTION_PREFIX, apiAtm.ActionsId); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if !exists {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_BROKEN_REFERENCE, err.Error())
|
||||
@@ -335,18 +360,21 @@ func (self *ApierV1) SetActionTimings(attrs AttrSetActionTimings, reply *string)
|
||||
timing.WeekDays.Parse(apiAtm.WeekDays, ";")
|
||||
timing.StartTime = apiAtm.Time
|
||||
at := &engine.ActionTiming{
|
||||
Id: utils.GenUUID(),
|
||||
Tag: attrs.ActionTimingsId,
|
||||
Uuid: utils.GenUUID(),
|
||||
Id: attrs.Id,
|
||||
Weight: apiAtm.Weight,
|
||||
Timing: &engine.RateInterval{Timing: timing},
|
||||
ActionsId: apiAtm.ActionsId,
|
||||
}
|
||||
storeAtms[idx] = at
|
||||
}
|
||||
if err := self.AccountDb.SetActionTimings(attrs.ActionTimingsId, storeAtms); err != nil {
|
||||
if err := self.AccountDb.SetActionTimings(attrs.Id, storeAtms); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
if attrs.ReloadScheduler && self.Sched != nil {
|
||||
if attrs.ReloadScheduler {
|
||||
if self.Sched == nil {
|
||||
return errors.New("SCHEDULER_NOT_ENABLED")
|
||||
}
|
||||
self.Sched.LoadActionTimings(self.AccountDb)
|
||||
self.Sched.Restart()
|
||||
}
|
||||
@@ -358,7 +386,7 @@ type AttrAddActionTrigger struct {
|
||||
Tenant string
|
||||
Account string
|
||||
Direction string
|
||||
BalanceId string
|
||||
BalanceType string
|
||||
ThresholdType string
|
||||
ThresholdValue float64
|
||||
DestinationId string
|
||||
@@ -373,7 +401,7 @@ func (self *ApierV1) AddTriggeredAction(attr AttrAddActionTrigger, reply *string
|
||||
|
||||
at := &engine.ActionTrigger{
|
||||
Id: utils.GenUUID(),
|
||||
BalanceId: attr.BalanceId,
|
||||
BalanceType: attr.BalanceType,
|
||||
Direction: attr.Direction,
|
||||
ThresholdType: attr.ThresholdType,
|
||||
ThresholdValue: attr.ThresholdValue,
|
||||
@@ -385,14 +413,14 @@ func (self *ApierV1) AddTriggeredAction(attr AttrAddActionTrigger, reply *string
|
||||
|
||||
tag := utils.BalanceKey(attr.Tenant, attr.Account, attr.Direction)
|
||||
_, err := engine.AccLock.Guard(tag, func() (float64, error) {
|
||||
userBalance, err := self.AccountDb.GetUserBalance(tag)
|
||||
userBalance, err := self.AccountDb.GetAccount(tag)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
userBalance.ActionTriggers = append(userBalance.ActionTriggers, at)
|
||||
|
||||
if err = self.AccountDb.SetUserBalance(userBalance); err != nil {
|
||||
if err = self.AccountDb.SetAccount(userBalance); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
@@ -420,6 +448,11 @@ func (self *ApierV1) LoadAccountActions(attrs utils.TPAccountActions, reply *str
|
||||
}); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
// ToDo: Get the action keys loaded by dbReader so we reload only these in cache
|
||||
// Need to do it before scheduler otherwise actions to run will be unknown
|
||||
if err := self.AccountDb.CacheAccounting(nil, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if self.Sched != nil {
|
||||
self.Sched.LoadActionTimings(self.AccountDb)
|
||||
self.Sched.Restart()
|
||||
@@ -436,11 +469,11 @@ func (self *ApierV1) ReloadScheduler(input string, reply *string) error {
|
||||
self.Sched.Restart()
|
||||
*reply = OK
|
||||
return nil
|
||||
|
||||
|
||||
}
|
||||
|
||||
func (self *ApierV1) ReloadCache(attrs utils.ApiReloadCache, reply *string) error {
|
||||
var dstKeys, rpKeys, rpfKeys, actKeys []string
|
||||
var dstKeys, rpKeys, rpfKeys, actKeys, shgKeys, rpAlsKeys, accAlsKeys []string
|
||||
if len(attrs.DestinationIds) > 0 {
|
||||
dstKeys = make([]string, len(attrs.DestinationIds))
|
||||
for idx, dId := range attrs.DestinationIds {
|
||||
@@ -465,10 +498,28 @@ func (self *ApierV1) ReloadCache(attrs utils.ApiReloadCache, reply *string) erro
|
||||
actKeys[idx] = engine.ACTION_PREFIX + actId
|
||||
}
|
||||
}
|
||||
if err := self.RatingDb.CacheRating(dstKeys, rpKeys, rpfKeys); err != nil {
|
||||
if len(attrs.SharedGroupIds) > 0 {
|
||||
shgKeys = make([]string, len(attrs.SharedGroupIds))
|
||||
for idx, shgId := range attrs.SharedGroupIds {
|
||||
shgKeys[idx] = engine.SHARED_GROUP_PREFIX + shgId
|
||||
}
|
||||
}
|
||||
if len(attrs.RpAliases) > 0 {
|
||||
rpAlsKeys = make([]string, len(attrs.RpAliases))
|
||||
for idx, alias := range attrs.RpAliases {
|
||||
rpAlsKeys[idx] = engine.RP_ALIAS_PREFIX + alias
|
||||
}
|
||||
}
|
||||
if len(attrs.AccAliases) > 0 {
|
||||
accAlsKeys = make([]string, len(attrs.AccAliases))
|
||||
for idx, alias := range attrs.AccAliases {
|
||||
accAlsKeys[idx] = engine.ACC_ALIAS_PREFIX + alias
|
||||
}
|
||||
}
|
||||
if err := self.RatingDb.CacheRating(dstKeys, rpKeys, rpfKeys, rpAlsKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.AccountDb.CacheAccounting(actKeys); err != nil {
|
||||
if err := self.AccountDb.CacheAccounting(actKeys, shgKeys, accAlsKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = "OK"
|
||||
@@ -481,6 +532,9 @@ func (self *ApierV1) GetCacheStats(attrs utils.AttrCacheStats, reply *utils.Cach
|
||||
cs.RatingPlans = cache2go.CountEntries(engine.RATING_PLAN_PREFIX)
|
||||
cs.RatingProfiles = cache2go.CountEntries(engine.RATING_PROFILE_PREFIX)
|
||||
cs.Actions = cache2go.CountEntries(engine.ACTION_PREFIX)
|
||||
cs.SharedGroups = cache2go.CountEntries(engine.SHARED_GROUP_PREFIX)
|
||||
cs.RatingAliases = cache2go.CountEntries(engine.RP_ALIAS_PREFIX)
|
||||
cs.AccountAliases = cache2go.CountEntries(engine.ACC_ALIAS_PREFIX)
|
||||
*reply = *cs
|
||||
return nil
|
||||
}
|
||||
@@ -492,7 +546,7 @@ func (self *ApierV1) GetCachedItemAge(itemId string, reply *utils.CachedItemAge)
|
||||
cachedItemAge := new(utils.CachedItemAge)
|
||||
var found bool
|
||||
for idx, cacheKey := range []string{engine.DESTINATION_PREFIX + itemId, engine.RATING_PLAN_PREFIX + itemId, engine.RATING_PROFILE_PREFIX + itemId,
|
||||
engine.ACTION_PREFIX + itemId} {
|
||||
engine.ACTION_PREFIX + itemId, engine.SHARED_GROUP_PREFIX + itemId, engine.RP_ALIAS_PREFIX + itemId, engine.ACC_ALIAS_PREFIX + itemId} {
|
||||
if age, err := cache2go.GetKeyAge(cacheKey); err == nil {
|
||||
found = true
|
||||
switch idx {
|
||||
@@ -504,6 +558,12 @@ func (self *ApierV1) GetCachedItemAge(itemId string, reply *utils.CachedItemAge)
|
||||
cachedItemAge.RatingProfile = age
|
||||
case 3:
|
||||
cachedItemAge.Action = age
|
||||
case 4:
|
||||
cachedItemAge.SharedGroup = age
|
||||
case 5:
|
||||
cachedItemAge.RatingAlias = age
|
||||
case 6:
|
||||
cachedItemAge.AccountAlias = age
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,13 +574,7 @@ func (self *ApierV1) GetCachedItemAge(itemId string, reply *utils.CachedItemAge)
|
||||
return nil
|
||||
}
|
||||
|
||||
type AttrLoadTPFromFolder struct {
|
||||
FolderPath string // Take files from folder absolute path
|
||||
DryRun bool // Do not write to database but parse only
|
||||
FlushDb bool // Flush previous data before loading new one
|
||||
}
|
||||
|
||||
func (self *ApierV1) LoadTariffPlanFromFolder(attrs AttrLoadTPFromFolder, reply *string) error {
|
||||
func (self *ApierV1) LoadTariffPlanFromFolder(attrs utils.AttrLoadTpFromFolder, reply *string) error {
|
||||
loader := engine.NewFileCSVReader(self.RatingDb, self.AccountDb, utils.CSV_SEP,
|
||||
path.Join(attrs.FolderPath, utils.DESTINATIONS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.TIMINGS_CSV),
|
||||
@@ -528,8 +582,9 @@ func (self *ApierV1) LoadTariffPlanFromFolder(attrs AttrLoadTPFromFolder, reply
|
||||
path.Join(attrs.FolderPath, utils.DESTINATION_RATES_CSV),
|
||||
path.Join(attrs.FolderPath, utils.RATING_PLANS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.RATING_PROFILES_CSV),
|
||||
path.Join(attrs.FolderPath, utils.SHARED_GROUPS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.ACTIONS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.ACTION_TIMINGS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.ACTION_PLANS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.ACTION_TRIGGERS_CSV),
|
||||
path.Join(attrs.FolderPath, utils.ACCOUNT_ACTIONS_CSV))
|
||||
if err := loader.LoadAll(); err != nil {
|
||||
@@ -563,10 +618,25 @@ func (self *ApierV1) LoadTariffPlanFromFolder(attrs AttrLoadTPFromFolder, reply
|
||||
for idx, actId := range actIds {
|
||||
actKeys[idx] = engine.ACTION_PREFIX + actId
|
||||
}
|
||||
if err := self.RatingDb.CacheRating(dstKeys, rpKeys, rpfKeys); err != nil {
|
||||
shgIds, _ := loader.GetLoadedIds(engine.SHARED_GROUP_PREFIX)
|
||||
shgKeys := make([]string, len(shgIds))
|
||||
for idx, shgId := range shgIds {
|
||||
shgKeys[idx] = engine.SHARED_GROUP_PREFIX + shgId
|
||||
}
|
||||
rpAliases, _ := loader.GetLoadedIds(engine.RP_ALIAS_PREFIX)
|
||||
rpAlsKeys := make([]string, len(rpAliases))
|
||||
for idx, alias := range rpAliases {
|
||||
rpAlsKeys[idx] = engine.RP_ALIAS_PREFIX + alias
|
||||
}
|
||||
accAliases, _ := loader.GetLoadedIds(engine.ACC_ALIAS_PREFIX)
|
||||
accAlsKeys := make([]string, len(accAliases))
|
||||
for idx, alias := range accAliases {
|
||||
accAlsKeys[idx] = engine.ACC_ALIAS_PREFIX + alias
|
||||
}
|
||||
if err := self.RatingDb.CacheRating(dstKeys, rpKeys, rpfKeys, rpAlsKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.AccountDb.CacheAccounting(actKeys); err != nil {
|
||||
if err := self.AccountDb.CacheAccounting(actKeys, shgKeys, accAlsKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
if self.Sched != nil {
|
||||
@@ -19,21 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package apier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
// ToDo: Replace rpc.Client with internal rpc server and Apier using internal map as both data and stor so we can run the tests non-local
|
||||
@@ -52,19 +55,31 @@ README:
|
||||
* Execute remote Apis and test their replies(follow prepaid1cent scenario so we can test load in dataDb also).
|
||||
*/
|
||||
|
||||
var cfgPath string
|
||||
var cfg *config.CGRConfig
|
||||
var rater *rpc.Client
|
||||
|
||||
var testLocal = flag.Bool("local", false, "Perform the tests only on local test environment, not by default.") // This flag will be passed here via "go test -local" args
|
||||
var dataDir = flag.String("data_dir", "/usr/share/cgrates", "CGR data dir path here")
|
||||
var storDbType = flag.String("stordb_type", "mysql", "The type of the storDb database <mysql>")
|
||||
var waitRater = flag.Int("wait_rater", 300, "Number of miliseconds to wait for rater to start and cache")
|
||||
var waitRater = flag.Int("wait_rater", 500, "Number of miliseconds to wait for rater to start and cache")
|
||||
|
||||
func init() {
|
||||
cfgPath := path.Join(*dataDir, "conf", "cgrates.cfg")
|
||||
cfgPath = path.Join(*dataDir, "conf", "samples", "apier_local_test.cfg")
|
||||
cfg, _ = config.NewCGRConfig(&cfgPath)
|
||||
}
|
||||
|
||||
func TestCreateDirs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
for _, pathDir := range []string{cfg.CdreDir, cfg.CdrcCdrInDir, cfg.CdrcCdrOutDir, cfg.HistoryDir} {
|
||||
if err := os.RemoveAll(pathDir); err != nil {
|
||||
t.Fatal("Error removing folder: ", pathDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty tables before using them
|
||||
func TestCreateTables(t *testing.T) {
|
||||
if !*testLocal {
|
||||
@@ -79,7 +94,8 @@ func TestCreateTables(t *testing.T) {
|
||||
} else {
|
||||
mysql = d.(*engine.MySQLStorage)
|
||||
}
|
||||
for _, scriptName := range []string{engine.CREATE_CDRS_TABLES_SQL, engine.CREATE_COSTDETAILS_TABLES_SQL, engine.CREATE_MEDIATOR_TABLES_SQL, engine.CREATE_TARIFFPLAN_TABLES_SQL} {
|
||||
for _, scriptName := range []string{engine.CREATE_CDRS_TABLES_SQL, engine.CREATE_COSTDETAILS_TABLES_SQL, engine.CREATE_MEDIATOR_TABLES_SQL,
|
||||
engine.CREATE_TARIFFPLAN_TABLES_SQL} {
|
||||
if err := mysql.CreateTablesFromScript(path.Join(*dataDir, "storage", *storDbType, scriptName)); err != nil {
|
||||
t.Fatal("Error on mysql creation: ", err.Error())
|
||||
return // No point in going further
|
||||
@@ -122,7 +138,7 @@ func TestStartEngine(t *testing.T) {
|
||||
t.Fatal("Cannot find cgr-engine executable")
|
||||
}
|
||||
exec.Command("pkill", "cgr-engine").Run() // Just to make sure another one is not running, bit brutal maybe we can fine tune it
|
||||
engine := exec.Command(enginePath, "-rater", "-scheduler", "-cdrs", "-mediator", "-config", path.Join(*dataDir, "conf", "cgrates.cfg"))
|
||||
engine := exec.Command(enginePath, "-config", cfgPath)
|
||||
if err := engine.Start(); err != nil {
|
||||
t.Fatal("Cannot start cgr-engine: ", err.Error())
|
||||
}
|
||||
@@ -135,11 +151,7 @@ func TestRpcConn(t *testing.T) {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if cfg.RPCEncoding == utils.JSON {
|
||||
rater, err = jsonrpc.Dial("tcp", cfg.MediatorRater)
|
||||
} else {
|
||||
rater, err = rpc.Dial("tcp", cfg.MediatorRater)
|
||||
}
|
||||
rater, err = jsonrpc.Dial("tcp", fscsvCfg.RPCJSONListen) // We connect over JSON so we can also troubleshoot if needed
|
||||
if err != nil {
|
||||
t.Fatal("Could not connect to rater: ", err.Error())
|
||||
}
|
||||
@@ -544,56 +556,56 @@ func TestApierTPActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApierTPActionTimings(t *testing.T) {
|
||||
func TestApierTPActionPlan(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
at := &utils.TPActionTimings{TPid: engine.TEST_SQL, ActionTimingsId: "PREPAID_10", ActionTimings: []*utils.TPActionTiming{
|
||||
at := &utils.TPActionPlan{TPid: engine.TEST_SQL, Id: "PREPAID_10", ActionPlan: []*utils.TPActionTiming{
|
||||
&utils.TPActionTiming{ActionsId: "PREPAID_10", TimingId: "ASAP", Weight: 10},
|
||||
}}
|
||||
atTst := new(utils.TPActionTimings)
|
||||
atTst := new(utils.TPActionPlan)
|
||||
*atTst = *at
|
||||
atTst.ActionTimingsId = engine.TEST_SQL
|
||||
for _, act := range []*utils.TPActionTimings{at, atTst} {
|
||||
if err := rater.Call("ApierV1.SetTPActionTimings", act, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetTPActionTimings: ", err.Error())
|
||||
atTst.Id = engine.TEST_SQL
|
||||
for _, act := range []*utils.TPActionPlan{at, atTst} {
|
||||
if err := rater.Call("ApierV1.SetTPActionPlan", act, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetTPActionPlan: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Unexpected reply received when calling ApierV1.SetTPActionTimings: ", reply)
|
||||
t.Error("Unexpected reply received when calling ApierV1.SetTPActionPlan: ", reply)
|
||||
}
|
||||
}
|
||||
// Check second set
|
||||
if err := rater.Call("ApierV1.SetTPActionTimings", atTst, &reply); err != nil {
|
||||
t.Error("Got error on second ApierV1.SetTPActionTimings: ", err.Error())
|
||||
if err := rater.Call("ApierV1.SetTPActionPlan", atTst, &reply); err != nil {
|
||||
t.Error("Got error on second ApierV1.SetTPActionPlan: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.SetTPActionTimings got reply: ", reply)
|
||||
t.Error("Calling ApierV1.SetTPActionPlan got reply: ", reply)
|
||||
}
|
||||
// Check missing params
|
||||
if err := rater.Call("ApierV1.SetTPActionTimings", new(utils.TPActionTimings), &reply); err == nil {
|
||||
t.Error("Calling ApierV1.SetTPActionTimings, expected error, received: ", reply)
|
||||
} else if err.Error() != "MANDATORY_IE_MISSING:[TPid ActionTimingsId ActionTimings]" {
|
||||
t.Error("Calling ApierV1.SetTPActionTimings got unexpected error: ", err.Error())
|
||||
if err := rater.Call("ApierV1.SetTPActionPlan", new(utils.TPActionPlan), &reply); err == nil {
|
||||
t.Error("Calling ApierV1.SetTPActionPlan, expected error, received: ", reply)
|
||||
} else if err.Error() != "MANDATORY_IE_MISSING:[TPid Id ActionPlan]" {
|
||||
t.Error("Calling ApierV1.SetTPActionPlan got unexpected error: ", err.Error())
|
||||
}
|
||||
// Test get
|
||||
var rplyActs *utils.TPActionTimings
|
||||
if err := rater.Call("ApierV1.GetTPActionTimings", AttrGetTPActionTimings{TPid: atTst.TPid, ActionTimingsId: atTst.ActionTimingsId}, &rplyActs); err != nil {
|
||||
t.Error("Calling ApierV1.GetTPActionTimings, got error: ", err.Error())
|
||||
var rplyActs *utils.TPActionPlan
|
||||
if err := rater.Call("ApierV1.GetTPActionPlan", AttrGetTPActionPlan{TPid: atTst.TPid, Id: atTst.Id}, &rplyActs); err != nil {
|
||||
t.Error("Calling ApierV1.GetTPActionPlan, got error: ", err.Error())
|
||||
} else if !reflect.DeepEqual(atTst, rplyActs) {
|
||||
t.Errorf("Calling ApierV1.GetTPActionTimings expected: %v, received: %v", atTst, rplyActs)
|
||||
t.Errorf("Calling ApierV1.GetTPActionPlan expected: %v, received: %v", atTst, rplyActs)
|
||||
}
|
||||
// Test remove
|
||||
if err := rater.Call("ApierV1.RemTPActionTimings", AttrGetTPActionTimings{TPid: atTst.TPid, ActionTimingsId: atTst.ActionTimingsId}, &reply); err != nil {
|
||||
t.Error("Calling ApierV1.RemTPActionTimings, got error: ", err.Error())
|
||||
if err := rater.Call("ApierV1.RemTPActionPlan", AttrGetTPActionPlan{TPid: atTst.TPid, Id: atTst.Id}, &reply); err != nil {
|
||||
t.Error("Calling ApierV1.RemTPActionPlan, got error: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.RemTPActionTimings received: ", reply)
|
||||
t.Error("Calling ApierV1.RemTPActionPlan received: ", reply)
|
||||
}
|
||||
// Test getIds
|
||||
var rplyIds []string
|
||||
expectedIds := []string{"PREPAID_10"}
|
||||
if err := rater.Call("ApierV1.GetTPActionTimingIds", AttrGetTPActionTimingIds{TPid: atTst.TPid}, &rplyIds); err != nil {
|
||||
t.Error("Calling ApierV1.GetTPActionTimingIds, got error: ", err.Error())
|
||||
if err := rater.Call("ApierV1.GetTPActionPlanIds", AttrGetTPActionPlanIds{TPid: atTst.TPid}, &rplyIds); err != nil {
|
||||
t.Error("Calling ApierV1.GetTPActionPlanIds, got error: ", err.Error())
|
||||
} else if !reflect.DeepEqual(expectedIds, rplyIds) {
|
||||
t.Errorf("Calling ApierV1.GetTPActionTimingIds expected: %v, received: %v", expectedIds, rplyIds)
|
||||
t.Errorf("Calling ApierV1.GetTPActionPlanIds expected: %v, received: %v", expectedIds, rplyIds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,15 +669,15 @@ func TestApierTPAccountActions(t *testing.T) {
|
||||
}
|
||||
reply := ""
|
||||
aa1 := &utils.TPAccountActions{TPid: engine.TEST_SQL, LoadId: engine.TEST_SQL, Tenant: "cgrates.org",
|
||||
Account: "1001", Direction: "*out", ActionTimingsId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
Account: "1001", Direction: "*out", ActionPlanId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
aa2 := &utils.TPAccountActions{TPid: engine.TEST_SQL, LoadId: engine.TEST_SQL, Tenant: "cgrates.org",
|
||||
Account: "1002", Direction: "*out", ActionTimingsId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
Account: "1002", Direction: "*out", ActionPlanId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
aa3 := &utils.TPAccountActions{TPid: engine.TEST_SQL, LoadId: engine.TEST_SQL, Tenant: "cgrates.org",
|
||||
Account: "1003", Direction: "*out", ActionTimingsId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
Account: "1003", Direction: "*out", ActionPlanId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
aa4 := &utils.TPAccountActions{TPid: engine.TEST_SQL, LoadId: engine.TEST_SQL, Tenant: "cgrates.org",
|
||||
Account: "1004", Direction: "*out", ActionTimingsId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
Account: "1004", Direction: "*out", ActionPlanId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
aa5 := &utils.TPAccountActions{TPid: engine.TEST_SQL, LoadId: engine.TEST_SQL, Tenant: "cgrates.org",
|
||||
Account: "1005", Direction: "*out", ActionTimingsId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
Account: "1005", Direction: "*out", ActionPlanId: "PREPAID_10", ActionTriggersId: "STANDARD_TRIGGERS"}
|
||||
aaTst := new(utils.TPAccountActions)
|
||||
*aaTst = *aa1
|
||||
aaTst.Account = engine.TEST_SQL
|
||||
@@ -685,7 +697,7 @@ func TestApierTPAccountActions(t *testing.T) {
|
||||
// Check missing params
|
||||
if err := rater.Call("ApierV1.SetTPAccountActions", new(utils.TPAccountActions), &reply); err == nil {
|
||||
t.Error("Calling ApierV1.SetTPAccountActions, expected error, received: ", reply)
|
||||
} else if err.Error() != "MANDATORY_IE_MISSING:[TPid LoadId Tenant Account Direction ActionTimingsId ActionTriggersId]" {
|
||||
} else if err.Error() != "MANDATORY_IE_MISSING:[TPid LoadId Tenant Account Direction ActionPlanId ActionTriggersId]" {
|
||||
t.Error("Calling ApierV1.SetTPAccountActions got unexpected error: ", err.Error())
|
||||
}
|
||||
// Test get
|
||||
@@ -724,6 +736,48 @@ func TestApierLoadRatingPlan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test here SetRatingProfile
|
||||
func TestApierSetRatingProfile(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
rpa := &utils.TPRatingActivation{ActivationTime: "2012-01-01T00:00:00Z", RatingPlanId: "RETAIL1", FallbackSubjects: "dan2"}
|
||||
rpf := &AttrSetRatingProfile{Tenant: "cgrates.org", TOR: "call", Direction: "*out", Subject: "dan", RatingPlanActivations: []*utils.TPRatingActivation{rpa}}
|
||||
if err := rater.Call("ApierV1.SetRatingProfile", rpf, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetRatingProfile: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.SetRatingProfile got reply: ", reply)
|
||||
}
|
||||
// Calling the second time should raise EXISTS
|
||||
if err := rater.Call("ApierV1.SetRatingProfile", rpf, &reply); err == nil || err.Error() != "EXISTS" {
|
||||
t.Error("Unexpected result on duplication: ", err.Error())
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond) // Give time for cache reload
|
||||
// Make sure rates were loaded for account dan
|
||||
// Test here ResponderGetCost
|
||||
tStart, _ := utils.ParseDate("2013-08-07T17:30:00Z")
|
||||
tEnd, _ := utils.ParseDate("2013-08-07T17:31:30Z")
|
||||
cd := engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
TOR: "call",
|
||||
Tenant: "cgrates.org",
|
||||
Subject: "dan",
|
||||
Account: "dan",
|
||||
Destination: "+4917621621391",
|
||||
CallDuration: 90,
|
||||
TimeStart: tStart,
|
||||
TimeEnd: tEnd,
|
||||
}
|
||||
var cc engine.CallCost
|
||||
// Simple test that command is executed without errors
|
||||
if err := rater.Call("Responder.GetCost", cd, &cc); err != nil {
|
||||
t.Error("Got error on Responder.GetCost: ", err.Error())
|
||||
} else if cc.Cost != 0 {
|
||||
t.Errorf("Calling Responder.GetCost got callcost: %v", cc.Cost)
|
||||
}
|
||||
}
|
||||
|
||||
// Test here LoadRatingProfile
|
||||
func TestApierLoadRatingProfile(t *testing.T) {
|
||||
if !*testLocal {
|
||||
@@ -738,25 +792,6 @@ func TestApierLoadRatingProfile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test here SetRatingProfile
|
||||
func TestApierSetRatingProfile(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
rpa := &utils.TPRatingActivation{ActivationTime: "2012-01-01T00:00:00Z", RatingPlanId: "RETAIL1", FallbackSubjects: "dan2;*any"}
|
||||
rpf := &AttrSetRatingProfile{Tenant: "cgrates.org", TOR: "call", Direction: "*out", Subject: "dan", RatingPlanActivations: []*utils.TPRatingActivation{rpa}}
|
||||
if err := rater.Call("ApierV1.SetRatingProfile", rpf, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetRatingProfile: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.SetRatingProfile got reply: ", reply)
|
||||
}
|
||||
// Calling the second time should raise EXISTS
|
||||
if err := rater.Call("ApierV1.SetRatingProfile", rpf, &reply); err == nil || err.Error() != "EXISTS" {
|
||||
t.Error("Unexpected result on duplication: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Test here LoadAccountActions
|
||||
func TestApierLoadAccountActions(t *testing.T) {
|
||||
if !*testLocal {
|
||||
@@ -875,12 +910,14 @@ func TestApierGetRatingPlan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
*/
|
||||
riRate := &engine.RIRate{ConnectFee: 0, RoundingMethod: "*up", RoundingDecimals: 0, Rates: []*engine.Rate{
|
||||
riRate := &engine.RIRate{Id: "RT_FS_USERS", ConnectFee: 0, RoundingMethod: "*up", RoundingDecimals: 0, Rates: []*engine.Rate{
|
||||
&engine.Rate{GroupIntervalStart: 0, Value: 0, RateIncrement: time.Duration(60) * time.Second, RateUnit: time.Duration(60) * time.Second},
|
||||
}}
|
||||
for _, rating := range reply.Ratings {
|
||||
riRateJsson, _ := json.Marshal(rating)
|
||||
if !reflect.DeepEqual(rating, riRate) {
|
||||
t.Errorf("Unexpected riRate received: %v", rating)
|
||||
t.Errorf("Unexpected riRate received: %s", riRateJsson)
|
||||
// {"Id":"RT_FS_USERS","ConnectFee":0,"Rates":[{"GroupIntervalStart":0,"Value":0,"RateIncrement":60000000000,"RateUnit":60000000000}],"RoundingMethod":"*up","RoundingDecimals":0}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,50 +928,49 @@ func TestApierAddBalance(t *testing.T) {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
attrs := &AttrAddBalance{Tenant: "cgrates.org", Account: "1001", BalanceId: "*monetary", Direction: "*out", Value: 1.5}
|
||||
attrs := &AttrAddBalance{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out", Value: 1.5}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan", BalanceId: "*monetary", Direction: "*out", Value: 1.5}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan", BalanceType: "*monetary", Direction: "*out", Value: 1.5}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan2", BalanceId: "*monetary", Direction: "*out", Value: 1.5}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan2", BalanceType: "*monetary", Direction: "*out", Value: 1.5}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan3", BalanceId: "*monetary", Direction: "*out", Value: 1.5}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan3", BalanceType: "*monetary", Direction: "*out", Value: 1.5}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan3", BalanceId: "*monetary", Direction: "*out", Value: 2.1}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan3", BalanceType: "*monetary", Direction: "*out", Value: 2.1}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan6", BalanceId: "*monetary", Direction: "*out", Value: 2.1}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan6", BalanceType: "*monetary", Direction: "*out", Value: 2.1}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan6", BalanceId: "*monetary", Direction: "*out", Value: 1, Overwrite: true}
|
||||
attrs = &AttrAddBalance{Tenant: "cgrates.org", Account: "dan6", BalanceType: "*monetary", Direction: "*out", Value: 1, Overwrite: true}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Test here ExecuteAction
|
||||
@@ -976,20 +1012,20 @@ func TestApierSetActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApierSetActionTimings(t *testing.T) {
|
||||
func TestApierSetActionPlan(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
atm1 := &ApiActionTiming{ActionsId: "ACTS_1", MonthDays: "1", Time: "00:00:00", Weight: 20.0}
|
||||
atms1 := &AttrSetActionTimings{ActionTimingsId: "ATMS_1", ActionTimings: []*ApiActionTiming{atm1}}
|
||||
atms1 := &AttrSetActionPlan{Id: "ATMS_1", ActionPlan: []*ApiActionTiming{atm1}}
|
||||
reply1 := ""
|
||||
if err := rater.Call("ApierV1.SetActionTimings", atms1, &reply1); err != nil {
|
||||
t.Error("Got error on ApierV1.SetActionTimings: ", err.Error())
|
||||
if err := rater.Call("ApierV1.SetActionPlan", atms1, &reply1); err != nil {
|
||||
t.Error("Got error on ApierV1.SetActionPlan: ", err.Error())
|
||||
} else if reply1 != "OK" {
|
||||
t.Errorf("Calling ApierV1.SetActionTimings received: %s", reply1)
|
||||
t.Errorf("Calling ApierV1.SetActionPlan received: %s", reply1)
|
||||
}
|
||||
// Calling the second time should raise EXISTS
|
||||
if err := rater.Call("ApierV1.SetActionTimings", atms1, &reply1); err == nil || err.Error() != "EXISTS" {
|
||||
if err := rater.Call("ApierV1.SetActionPlan", atms1, &reply1); err == nil || err.Error() != "EXISTS" {
|
||||
t.Error("Unexpected result on duplication: ", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1001,7 +1037,7 @@ func TestApierAddTriggeredAction(t *testing.T) {
|
||||
}
|
||||
reply := ""
|
||||
// Add balance to a previously known account
|
||||
attrs := &AttrAddActionTrigger{Tenant: "cgrates.org", Account: "dan2", Direction: "*out", BalanceId: "*monetary",
|
||||
attrs := &AttrAddActionTrigger{Tenant: "cgrates.org", Account: "dan2", Direction: "*out", BalanceType: "*monetary",
|
||||
ThresholdType: "*min_balance", ThresholdValue: 2, DestinationId: "*any", Weight: 10, ActionsId: "WARN_VIA_HTTP"}
|
||||
if err := rater.Call("ApierV1.AddTriggeredAction", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddTriggeredAction: ", err.Error())
|
||||
@@ -1018,15 +1054,13 @@ func TestApierAddTriggeredAction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Test here AddTriggeredAction
|
||||
// Test here GetAccountActionTriggers
|
||||
func TestApierGetAccountActionTriggers(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var reply engine.ActionTriggerPriotityList
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account:"dan2", Direction: "*out"}
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan2", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccountActionTriggers", req, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error())
|
||||
} else if len(reply) != 1 || reply[0].ActionsId != "WARN_VIA_HTTP" {
|
||||
@@ -1041,14 +1075,14 @@ func TestApierRemAccountActionTriggers(t *testing.T) {
|
||||
}
|
||||
// Test first get so we can steal the id which we need to remove
|
||||
var reply engine.ActionTriggerPriotityList
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account:"dan2", Direction: "*out"}
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan2", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccountActionTriggers", req, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error())
|
||||
} else if len(reply) != 1 || reply[0].ActionsId != "WARN_VIA_HTTP" {
|
||||
t.Errorf("Unexpected action triggers received %v", reply)
|
||||
}
|
||||
var rmReply string
|
||||
rmReq := AttrRemAcntActionTriggers{Tenant: "cgrates.org", Account:"dan2", Direction: "*out", ActionTriggerId: reply[0].Id}
|
||||
rmReq := AttrRemAcntActionTriggers{Tenant: "cgrates.org", Account: "dan2", Direction: "*out", ActionTriggerId: reply[0].Id}
|
||||
if err := rater.Call("ApierV1.RemAccountActionTriggers", rmReq, &rmReply); err != nil {
|
||||
t.Error("Got error on ApierV1.RemActionTiming: ", err.Error())
|
||||
} else if rmReply != OK {
|
||||
@@ -1061,44 +1095,42 @@ func TestApierRemAccountActionTriggers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Test here SetAccount
|
||||
func TestApierSetAccount(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
attrs := &AttrSetAccount{Tenant: "cgrates.org", Direction: "*out", Account: "dan7", Type: "*prepaid", ActionTimingsId: "ATMS_1"}
|
||||
if err := rater.Call("ApierV1.SetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetAccount: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.SetAccount received: %s", reply)
|
||||
}
|
||||
reply2 := ""
|
||||
attrs2 := new(AttrSetAccount)
|
||||
*attrs2 = *attrs
|
||||
attrs2.ActionTimingsId = "DUMMY_DATA" // Does not exist so it should error when adding triggers on it
|
||||
// Add account with actions timing which does not exist
|
||||
if err := rater.Call("ApierV1.SetAccount", attrs2, &reply2); err == nil || reply2 == "OK" { // OK is not welcomed
|
||||
t.Error("Expecting error on ApierV1.SetAccount.", err, reply2)
|
||||
}
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
attrs := &AttrSetAccount{Tenant: "cgrates.org", Direction: "*out", Account: "dan7", ActionPlanId: "ATMS_1"}
|
||||
if err := rater.Call("ApierV1.SetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetAccount: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.SetAccount received: %s", reply)
|
||||
}
|
||||
reply2 := ""
|
||||
attrs2 := new(AttrSetAccount)
|
||||
*attrs2 = *attrs
|
||||
attrs2.ActionPlanId = "DUMMY_DATA" // Does not exist so it should error when adding triggers on it
|
||||
// Add account with actions timing which does not exist
|
||||
if err := rater.Call("ApierV1.SetAccount", attrs2, &reply2); err == nil || reply2 == "OK" { // OK is not welcomed
|
||||
t.Error("Expecting error on ApierV1.SetAccount.", err, reply2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Test here GetAccountActionTimings
|
||||
func TestApierGetAccountActionTimings(t *testing.T) {
|
||||
func TestApierGetAccountActionPlan(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var reply []*AccountActionTiming
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account:"dan7", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccountActionTimings", req, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error())
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan7", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccountActionPlan", req, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccountActionPlan: ", err.Error())
|
||||
} else if len(reply) != 1 {
|
||||
t.Error("Unexpected action timings received")
|
||||
t.Error("Unexpected action plan received")
|
||||
} else {
|
||||
if reply[0].ActionTimingsId != "ATMS_1" {
|
||||
t.Errorf("Unexpected ActionTImingsId received")
|
||||
if reply[0].ActionPlanId != "ATMS_1" {
|
||||
t.Errorf("Unexpected ActionPlanId received")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1109,57 +1141,101 @@ func TestApierRemActionTiming(t *testing.T) {
|
||||
return
|
||||
}
|
||||
var rmReply string
|
||||
rmReq := AttrRemActionTiming{ActionTimingsId: "ATMS_1", Tenant: "cgrates.org", Account:"dan4", Direction: "*out"}
|
||||
rmReq := AttrRemActionTiming{ActionPlanId: "ATMS_1", Tenant: "cgrates.org", Account: "dan4", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.RemActionTiming", rmReq, &rmReply); err != nil {
|
||||
t.Error("Got error on ApierV1.RemActionTiming: ", err.Error())
|
||||
} else if rmReply != OK {
|
||||
t.Error("Unexpected answer received", rmReply)
|
||||
}
|
||||
var reply []*AccountActionTiming
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account:"dan4", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccountActionTimings", req, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccountActionTimings: ", err.Error())
|
||||
req := AttrAcntAction{Tenant: "cgrates.org", Account: "dan4", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccountActionPlan", req, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccountActionPlan: ", err.Error())
|
||||
} else if len(reply) != 0 {
|
||||
t.Error("Action timings was not removed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test here GetBalance
|
||||
func TestApierGetBalance(t *testing.T) {
|
||||
// Test here GetAccount
|
||||
func TestApierGetAccount(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var reply float64
|
||||
attrs := &AttrGetBalance{Tenant: "cgrates.org", Account: "1001", BalanceId: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetBalance: ", err.Error())
|
||||
} else if reply != 11.5 { // We expect 11.5 since we have added in the previous test 1.5
|
||||
var reply *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 11.5 { // We expect 11.5 since we have added in the previous test 1.5
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 11.5, received: %f", reply)
|
||||
}
|
||||
attrs = &AttrGetBalance{Tenant: "cgrates.org", Account: "dan", BalanceId: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetBalance: ", err.Error())
|
||||
} else if reply != 1.5 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 1.5, received: %f", reply)
|
||||
attrs = &AttrGetAccount{Tenant: "cgrates.org", Account: "dan", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 1.5 {
|
||||
t.Errorf("Calling ApierV1.GetAccount expected: 1.5, received: %f", reply)
|
||||
}
|
||||
// The one we have topped up though executeAction
|
||||
attrs = &AttrGetBalance{Tenant: "cgrates.org", Account: "dan2", BalanceId: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetBalance: ", err.Error())
|
||||
} else if reply != 10 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 10, received: %f", reply)
|
||||
attrs = &AttrGetAccount{Tenant: "cgrates.org", Account: "dan2", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 10 {
|
||||
t.Errorf("Calling ApierV1.GetAccount expected: 10, received: %f", reply)
|
||||
}
|
||||
attrs = &AttrGetBalance{Tenant: "cgrates.org", Account: "dan3", BalanceId: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetBalance: ", err.Error())
|
||||
} else if reply != 3.6 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 3.6, received: %f", reply)
|
||||
attrs = &AttrGetAccount{Tenant: "cgrates.org", Account: "dan3", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 3.6 {
|
||||
t.Errorf("Calling ApierV1.GetAccount expected: 3.6, received: %f", reply)
|
||||
}
|
||||
attrs = &AttrGetBalance{Tenant: "cgrates.org", Account: "dan6", BalanceId: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetBalance", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetBalance: ", err.Error())
|
||||
} else if reply != 1 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 1, received: %f", reply)
|
||||
attrs = &AttrGetAccount{Tenant: "cgrates.org", Account: "dan6", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 1 {
|
||||
t.Errorf("Calling ApierV1.GetAccount expected: 1, received: %f", reply)
|
||||
}
|
||||
}
|
||||
|
||||
// Start with initial balance, top-up to test max_balance
|
||||
func TestTriggersExecute(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
attrs := &AttrSetAccount{Tenant: "cgrates.org", Direction: "*out", Account: "dan8"}
|
||||
if err := rater.Call("ApierV1.SetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.SetAccount: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.SetAccount received: %s", reply)
|
||||
}
|
||||
attrAddBlnc := &AttrAddBalance{Tenant: "cgrates.org", Account: "1008", BalanceType: "*monetary", Direction: "*out", Value: 2}
|
||||
if err := rater.Call("ApierV1.AddBalance", attrAddBlnc, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.AddBalance: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Errorf("Calling ApierV1.AddBalance received: %s", reply)
|
||||
}
|
||||
}
|
||||
|
||||
// Start fresh before loading from folder
|
||||
func TestResetDataBeforeLoadFromFolder(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
TestInitDataDb(t)
|
||||
reply := ""
|
||||
arc := new(utils.ApiReloadCache)
|
||||
// Simple test that command is executed without errors
|
||||
if err := rater.Call("ApierV1.ReloadCache", arc, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.ReloadCache: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.ReloadCache got reply: ", reply)
|
||||
}
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{}
|
||||
var args utils.AttrCacheStats
|
||||
if err := rater.Call("ApierV1.GetCacheStats", args, &rcvStats); err != nil {
|
||||
t.Error("Got error on ApierV1.GetCacheStats: ", err.Error())
|
||||
} else if !reflect.DeepEqual(rcvStats, expectedStats) {
|
||||
t.Errorf("Calling ApierV1.GetCacheStats received: %v, expected: %v", rcvStats, expectedStats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,12 +1246,28 @@ func TestApierLoadTariffPlanFromFolder(t *testing.T) {
|
||||
}
|
||||
reply := ""
|
||||
// Simple test that command is executed without errors
|
||||
attrs := &AttrLoadTPFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "prepaid1centpsec")}
|
||||
attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "prepaid1centpsec")}
|
||||
if err := rater.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.LoadTariffPlanFromFolder: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.LoadTariffPlanFromFolder got reply: ", reply)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond) // Give time for scheduler to execute topups
|
||||
}
|
||||
|
||||
// Make sure balance was topped-up
|
||||
// Bug reported by DigiDaz over IRC
|
||||
func TestApierGetAccountAfterLoad(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var reply *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 11 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 11, received: %f", reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
}
|
||||
|
||||
// Test here ResponderGetCost
|
||||
@@ -1205,20 +1297,38 @@ func TestResponderGetCost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test here ResponderGetCost
|
||||
func TestGetCallCostLog(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var cc engine.CallCost
|
||||
var attrs AttrGetCallCost
|
||||
// Simple test that command is executed without errors
|
||||
if err := rater.Call("ApierV1.GetCallCostLog", attrs, &cc); err == nil {
|
||||
t.Error("Failed to detect missing fields in ApierV1.GetCallCostLog")
|
||||
}
|
||||
attrs.CgrId = "dummyid"
|
||||
attrs.RunId = "default"
|
||||
if err := rater.Call("ApierV1.GetCallCostLog", attrs, &cc); err == nil || err.Error() != utils.ERR_NOT_FOUND {
|
||||
t.Error("ApierV1.GetCallCostLog: should return NOT_FOUND")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdrServer(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
httpClient := new(http.Client)
|
||||
cdrForm1 := url.Values{"accid": []string{"dsafdsaf"}, "cdrhost": []string{"192.168.1.1"}, "reqtype": []string{"rated"}, "direction": []string{"*out"},
|
||||
"tenant": []string{"cgrates.org"}, "tor": []string{"call"}, "account": []string{"1001"}, "subject": []string{"1001"}, "destination": []string{"1002"},
|
||||
cdrForm1 := url.Values{"accid": []string{"dsafdsaf"}, "cdrhost": []string{"192.168.1.1"}, "reqtype": []string{"rated"}, "direction": []string{"*out"},
|
||||
"tenant": []string{"cgrates.org"}, "tor": []string{"call"}, "account": []string{"1001"}, "subject": []string{"1001"}, "destination": []string{"1002"},
|
||||
"answer_time": []string{"2013-11-07T08:42:26Z"}, "duration": []string{"10"}, "field_extr1": []string{"val_extr1"}, "fieldextr2": []string{"valextr2"}}
|
||||
cdrForm2 := url.Values{"accid": []string{"adsafdsaf"}, "cdrhost": []string{"192.168.1.1"}, "reqtype": []string{"rated"}, "direction": []string{"*out"},
|
||||
"tenant": []string{"cgrates.org"}, "tor": []string{"call"}, "account": []string{"1001"}, "subject": []string{"1001"}, "destination": []string{"1002"},
|
||||
cdrForm2 := url.Values{"accid": []string{"adsafdsaf"}, "cdrhost": []string{"192.168.1.1"}, "reqtype": []string{"rated"}, "direction": []string{"*out"},
|
||||
"tenant": []string{"cgrates.org"}, "tor": []string{"call"}, "account": []string{"1001"}, "subject": []string{"1001"}, "destination": []string{"1002"},
|
||||
"answer_time": []string{"2013-11-07T08:42:26Z"}, "duration": []string{"10"}, "field_extr1": []string{"val_extr1"}, "fieldextr2": []string{"valextr2"}}
|
||||
for _, cdrForm := range []url.Values{cdrForm1, cdrForm2} {
|
||||
cdrForm.Set(utils.CDRSOURCE, engine.TEST_SQL)
|
||||
if _, err := httpClient.PostForm(fmt.Sprintf("http://%s/cgr", cfg.CdrcCdrs), cdrForm); err != nil {
|
||||
if _, err := httpClient.PostForm(fmt.Sprintf("http://%s/cgr", "127.0.0.1:2080"), cdrForm); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1226,15 +1336,15 @@ func TestCdrServer(t *testing.T) {
|
||||
|
||||
func TestExportCdrsToFile(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
var reply *utils.ExportedFileCdrs
|
||||
req := utils.AttrExpFileCdrs{}
|
||||
if err := rater.Call("ApierV1.ExportCdrsToFile", req, &reply); err == nil || !strings.HasPrefix(err.Error(), utils.ERR_MANDATORY_IE_MISSING) {
|
||||
t.Error("Failed to detect missing parameter")
|
||||
}
|
||||
req.CdrFormat = utils.CDRE_DRYRUN
|
||||
expectReply := &utils.ExportedFileCdrs{NumberOfRecords: 2}
|
||||
expectReply := &utils.ExportedFileCdrs{ExportedFilePath: utils.CDRE_DRYRUN, TotalRecords: 2, ExportedCgrIds: []string{utils.FSCgrId("dsafdsaf"), utils.FSCgrId("adsafdsaf")}}
|
||||
if err := rater.Call("ApierV1.ExportCdrsToFile", req, &reply); err != nil {
|
||||
t.Error(err.Error())
|
||||
} else if !reflect.DeepEqual(reply, expectReply) {
|
||||
@@ -1256,7 +1366,6 @@ func TestExportCdrsToFile(t *testing.T) {
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// Simply kill the engine after we are done with tests within this file
|
||||
func TestStopEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
153
apier/cdre.go
Normal file
153
apier/cdre.go
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package apier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/cdre"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Export Cdrs to file
|
||||
func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.ExportedFileCdrs) error {
|
||||
var tStart, tEnd time.Time
|
||||
var err error
|
||||
cdrFormat := strings.ToLower(attr.CdrFormat)
|
||||
if !utils.IsSliceMember(utils.CdreCdrFormats, cdrFormat) {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, "CdrFormat")
|
||||
}
|
||||
if len(attr.TimeStart) != 0 {
|
||||
if tStart, err = utils.ParseTimeDetectLayout(attr.TimeStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(attr.TimeEnd) != 0 {
|
||||
if tEnd, err = utils.ParseTimeDetectLayout(attr.TimeEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fileName := attr.ExportFileName
|
||||
exportId := attr.ExportId
|
||||
if len(exportId) == 0 {
|
||||
exportId = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
}
|
||||
roundDecimals := attr.RoundingDecimals
|
||||
if roundDecimals == 0 {
|
||||
roundDecimals = self.Config.RoundingDecimals
|
||||
}
|
||||
cdrs, err := self.CdrDb.GetStoredCdrs(attr.CgrIds, attr.MediationRunId, attr.CdrHost, attr.CdrSource, attr.ReqType, attr.Direction,
|
||||
attr.Tenant, attr.Tor, attr.Account, attr.Subject, attr.DestinationPrefix, tStart, tEnd, attr.SkipErrors, attr.SkipRated)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(cdrs) == 0 {
|
||||
*reply = utils.ExportedFileCdrs{ExportedFilePath: ""}
|
||||
return nil
|
||||
}
|
||||
switch cdrFormat {
|
||||
case utils.CDRE_DRYRUN:
|
||||
exportedIds := make([]string, len(cdrs))
|
||||
for idxCdr, cdr := range cdrs {
|
||||
exportedIds[idxCdr] = cdr.CgrId
|
||||
}
|
||||
*reply = utils.ExportedFileCdrs{ExportedFilePath: utils.CDRE_DRYRUN, TotalRecords: len(cdrs), ExportedCgrIds: exportedIds}
|
||||
case utils.CDRE_CSV:
|
||||
if len(fileName) == 0 {
|
||||
fileName = fmt.Sprintf("cdre_%s.csv", exportId)
|
||||
}
|
||||
exportedFields := self.Config.CdreExportedFields
|
||||
if len(attr.ExportTemplate) != 0 {
|
||||
if exportedFields, err = config.ParseRSRFields(attr.ExportTemplate); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
}
|
||||
if len(exportedFields) == 0 {
|
||||
return fmt.Errorf("%s:ExportTemplate", utils.ERR_MANDATORY_IE_MISSING)
|
||||
}
|
||||
filePath := path.Join(self.Config.CdreDir, fileName)
|
||||
fileOut, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileOut.Close()
|
||||
csvWriter := cdre.NewCsvCdrWriter(fileOut, roundDecimals, exportedFields)
|
||||
exportedIds := make([]string, 0)
|
||||
unexportedIds := make(map[string]string)
|
||||
for _, cdr := range cdrs {
|
||||
if err := csvWriter.WriteCdr(cdr); err != nil {
|
||||
unexportedIds[cdr.CgrId] = err.Error()
|
||||
} else {
|
||||
exportedIds = append(exportedIds, cdr.CgrId)
|
||||
}
|
||||
}
|
||||
csvWriter.Close()
|
||||
*reply = utils.ExportedFileCdrs{ExportedFilePath: filePath, TotalRecords: len(cdrs), ExportedCgrIds: exportedIds, UnexportedCgrIds: unexportedIds}
|
||||
case utils.CDRE_FIXED_WIDTH:
|
||||
if len(fileName) == 0 {
|
||||
fileName = fmt.Sprintf("cdre_%s.fwv", exportId)
|
||||
}
|
||||
exportTemplate := self.Config.CdreFWXmlTemplate
|
||||
if len(attr.ExportTemplate) != 0 && self.Config.XmlCfgDocument != nil {
|
||||
if xmlTemplate, err := self.Config.XmlCfgDocument.GetCdreFWCfg(attr.ExportTemplate[len(utils.XML_PROFILE_PREFIX):]); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if xmlTemplate != nil {
|
||||
exportTemplate = xmlTemplate
|
||||
}
|
||||
}
|
||||
if exportTemplate == nil {
|
||||
return fmt.Errorf("%s:ExportTemplate", utils.ERR_MANDATORY_IE_MISSING)
|
||||
}
|
||||
filePath := path.Join(self.Config.CdreDir, fileName)
|
||||
fileOut, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileOut.Close()
|
||||
fww, _ := cdre.NewFWCdrWriter(self.LogDb, fileOut, exportTemplate, exportId, roundDecimals)
|
||||
exportedIds := make([]string, 0)
|
||||
unexportedIds := make(map[string]string)
|
||||
for _, cdr := range cdrs {
|
||||
if err := fww.WriteCdr(cdr); err != nil {
|
||||
unexportedIds[cdr.CgrId] = err.Error()
|
||||
} else {
|
||||
exportedIds = append(exportedIds, cdr.CgrId)
|
||||
}
|
||||
}
|
||||
fww.Close()
|
||||
*reply = utils.ExportedFileCdrs{ExportedFilePath: filePath, TotalRecords: len(cdrs), ExportedCgrIds: exportedIds, UnexportedCgrIds: unexportedIds}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove Cdrs out of CDR storage
|
||||
func (self *ApierV1) RemCdrs(attrs utils.AttrRemCdrs, reply *string) error {
|
||||
if len(attrs.CgrIds) == 0 {
|
||||
return fmt.Errorf("%s:CgrIds", utils.ERR_MANDATORY_IE_MISSING)
|
||||
}
|
||||
if err := self.CdrDb.RemStoredCdrs(attrs.CgrIds); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
*reply = "OK"
|
||||
return nil
|
||||
}
|
||||
45
apier/cdrs.go
Normal file
45
apier/cdrs.go
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package apier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
type AttrGetCallCost struct {
|
||||
CgrId string // Unique id of the CDR
|
||||
RunId string // Run Id
|
||||
}
|
||||
|
||||
// Retrieves the callCost out of CGR logDb
|
||||
func (apier *ApierV1) GetCallCostLog(attrs AttrGetCallCost, reply *engine.CallCost) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"CgrId", "RunId"}); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if cc, err := apier.LogDb.GetCallCostLog(attrs.CgrId, "", attrs.RunId); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if cc == nil {
|
||||
return fmt.Errorf("NOT_FOUND")
|
||||
} else {
|
||||
*reply = *cc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -21,13 +21,14 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
// Creates a new AccountActions profile within a tariff plan
|
||||
func (self *ApierV1) SetTPAccountActions(attrs utils.TPAccountActions, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs,
|
||||
[]string{"TPid", "LoadId", "Tenant", "Account", "Direction", "ActionTimingsId", "ActionTriggersId"}); len(missing) != 0 {
|
||||
[]string{"TPid", "LoadId", "Tenant", "Account", "Direction", "ActionPlanId", "ActionTriggersId"}); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if err := self.StorDb.SetTPAccountActions(attrs.TPid, map[string]*utils.TPAccountActions{attrs.KeyId(): &attrs}); err != nil {
|
||||
@@ -79,7 +80,7 @@ func (self *ApierV1) GetTPAccountActionLoadIds(attrs AttrGetTPAccountActionIds,
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPAccountActionIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_ACCOUNT_ACTIONS, "loadid", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -21,6 +21,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -74,7 +75,7 @@ func (self *ApierV1) GetTPActionIds(attrs AttrGetTPActionIds, reply *[]string) e
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPActionIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_ACTIONS, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -21,58 +21,59 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
// Creates a new ActionTimings profile within a tariff plan
|
||||
func (self *ApierV1) SetTPActionTimings(attrs utils.TPActionTimings, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid", "ActionTimingsId", "ActionTimings"}); len(missing) != 0 {
|
||||
func (self *ApierV1) SetTPActionPlan(attrs utils.TPActionPlan, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid", "Id", "ActionPlan"}); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
for _, at := range attrs.ActionTimings {
|
||||
for _, at := range attrs.ActionPlan {
|
||||
requiredFields := []string{"ActionsId", "TimingId", "Weight"}
|
||||
if missing := utils.MissingStructFields(at, requiredFields); len(missing) != 0 {
|
||||
return fmt.Errorf("%s:Action:%s:%v", utils.ERR_MANDATORY_IE_MISSING, at.ActionsId, missing)
|
||||
}
|
||||
}
|
||||
if err := self.StorDb.SetTPActionTimings(attrs.TPid, map[string][]*utils.TPActionTiming{attrs.ActionTimingsId: attrs.ActionTimings}); err != nil {
|
||||
if err := self.StorDb.SetTPActionTimings(attrs.TPid, map[string][]*utils.TPActionTiming{attrs.Id: attrs.ActionPlan}); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
}
|
||||
*reply = "OK"
|
||||
return nil
|
||||
}
|
||||
|
||||
type AttrGetTPActionTimings struct {
|
||||
TPid string // Tariff plan id
|
||||
ActionTimingsId string // ActionTimings id
|
||||
type AttrGetTPActionPlan struct {
|
||||
TPid string // Tariff plan id
|
||||
Id string // ActionTimings id
|
||||
}
|
||||
|
||||
// Queries specific ActionTimings profile on tariff plan
|
||||
func (self *ApierV1) GetTPActionTimings(attrs AttrGetTPActionTimings, reply *utils.TPActionTimings) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid", "ActionTimingsId"}); len(missing) != 0 { //Params missing
|
||||
// Queries specific ActionPlan profile on tariff plan
|
||||
func (self *ApierV1) GetTPActionPlan(attrs AttrGetTPActionPlan, reply *utils.TPActionPlan) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid", "Id"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ats, err := self.StorDb.GetTPActionTimings(attrs.TPid, attrs.ActionTimingsId); err != nil {
|
||||
if ats, err := self.StorDb.GetTPActionTimings(attrs.TPid, attrs.Id); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if len(ats) == 0 {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
} else { // Got the data we need, convert it
|
||||
atRply := &utils.TPActionTimings{attrs.TPid, attrs.ActionTimingsId, ats[attrs.ActionTimingsId]}
|
||||
atRply := &utils.TPActionPlan{attrs.TPid, attrs.Id, ats[attrs.Id]}
|
||||
*reply = *atRply
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AttrGetTPActionTimingIds struct {
|
||||
type AttrGetTPActionPlanIds struct {
|
||||
TPid string // Tariff plan id
|
||||
}
|
||||
|
||||
// Queries ActionTimings identities on specific tariff plan.
|
||||
func (self *ApierV1) GetTPActionTimingIds(attrs AttrGetTPActionTimingIds, reply *[]string) error {
|
||||
// Queries ActionPlan identities on specific tariff plan.
|
||||
func (self *ApierV1) GetTPActionPlanIds(attrs AttrGetTPActionPlanIds, reply *[]string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPActionTimingIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_ACTION_PLANS, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -82,12 +83,12 @@ func (self *ApierV1) GetTPActionTimingIds(attrs AttrGetTPActionTimingIds, reply
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes specific ActionTimings on Tariff plan
|
||||
func (self *ApierV1) RemTPActionTimings(attrs AttrGetTPActionTimings, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid", "ActionTimingsId"}); len(missing) != 0 { //Params missing
|
||||
// Removes specific ActionPlan on Tariff plan
|
||||
func (self *ApierV1) RemTPActionPlan(attrs AttrGetTPActionPlan, reply *string) error {
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid", "Id"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if err := self.StorDb.RemTPData(utils.TBL_TP_ACTION_TIMINGS, attrs.TPid, attrs.ActionTimingsId); err != nil {
|
||||
if err := self.StorDb.RemTPData(utils.TBL_TP_ACTION_PLANS, attrs.TPid, attrs.Id); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else {
|
||||
*reply = "OK"
|
||||
@@ -21,6 +21,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -70,7 +71,7 @@ func (self *ApierV1) GetTPActionTriggerIds(attrs AttrGetTPActionTriggerIds, repl
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPActionTriggerIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_ACTION_TRIGGERS, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -23,6 +23,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -67,7 +68,7 @@ func (self *ApierV1) GetTPDestinationRateIds(attrs AttrGetTPRateIds, reply *[]st
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPDestinationRateIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_DESTINATION_RATES, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -67,7 +67,7 @@ func (self *ApierV1) GetTPDestinationIds(attrs AttrGetTPDestinationIds, reply *[
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPDestinationIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_DESTINATIONS, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -23,6 +23,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -67,7 +68,7 @@ func (self *ApierV1) GetTPRateIds(attrs AttrGetTPRateIds, reply *[]string) error
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPRateIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_RATES, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -23,6 +23,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -67,7 +68,7 @@ func (self *ApierV1) GetTPRatingPlanIds(attrs AttrGetTPRatingPlanIds, reply *[]s
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPRatingPlanIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_RATING_PLANS, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -23,6 +23,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
@@ -75,7 +76,12 @@ func (self *ApierV1) GetTPRatingProfileLoadIds(attrs utils.AttrTPRatingProfileId
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPRatingProfileIds(&attrs); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_RATE_PROFILES, "loadid", map[string]string{
|
||||
"tenant": attrs.Tenant,
|
||||
"tor": attrs.TOR,
|
||||
"direction": attrs.Direction,
|
||||
"subject": attrs.Subject,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
@@ -21,6 +21,7 @@ package apier
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
@@ -69,7 +70,7 @@ func (self *ApierV1) GetTPTimingIds(attrs AttrGetTPTimingIds, reply *[]string) e
|
||||
if missing := utils.MissingStructFields(&attrs, []string{"TPid"}); len(missing) != 0 { //Params missing
|
||||
return fmt.Errorf("%s:%v", utils.ERR_MANDATORY_IE_MISSING, missing)
|
||||
}
|
||||
if ids, err := self.StorDb.GetTPTimingIds(attrs.TPid); err != nil {
|
||||
if ids, err := self.StorDb.GetTPTableIds(attrs.TPid, utils.TBL_TP_TIMINGS, "id", nil); err != nil {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
|
||||
} else if ids == nil {
|
||||
return errors.New(utils.ERR_NOT_FOUND)
|
||||
285
apier/tutfscsv_local_test.go
Normal file
285
apier/tutfscsv_local_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package apier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
var fscsvCfgPath string
|
||||
var fscsvCfg *config.CGRConfig
|
||||
|
||||
func init() {
|
||||
fscsvCfgPath = path.Join(*dataDir, "tutorials", "fs_csv", "cgrates", "etc", "cgrates", "cgrates.cfg")
|
||||
fscsvCfg, _ = config.NewCGRConfig(&fscsvCfgPath)
|
||||
}
|
||||
|
||||
// Remove here so they can be properly created by init script
|
||||
func TestFsCsvRemoveDirs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
for _, pathDir := range []string{cfg.CdreDir, cfg.CdrcCdrInDir, cfg.CdrcCdrOutDir, cfg.HistoryDir} {
|
||||
if err := os.RemoveAll(pathDir); err != nil {
|
||||
t.Fatal("Error removing folder: ", pathDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty tables before using them
|
||||
func TestFsCsvCreateTables(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
if *storDbType != utils.MYSQL {
|
||||
t.Fatal("Unsupported storDbType")
|
||||
}
|
||||
var mysql *engine.MySQLStorage
|
||||
if d, err := engine.NewMySQLStorage(fscsvCfg.StorDBHost, fscsvCfg.StorDBPort, fscsvCfg.StorDBName, fscsvCfg.StorDBUser, fscsvCfg.StorDBPass); err != nil {
|
||||
t.Fatal("Error on opening database connection: ", err)
|
||||
} else {
|
||||
mysql = d.(*engine.MySQLStorage)
|
||||
}
|
||||
for _, scriptName := range []string{engine.CREATE_CDRS_TABLES_SQL, engine.CREATE_COSTDETAILS_TABLES_SQL, engine.CREATE_MEDIATOR_TABLES_SQL, engine.CREATE_TARIFFPLAN_TABLES_SQL} {
|
||||
if err := mysql.CreateTablesFromScript(path.Join(*dataDir, "storage", *storDbType, scriptName)); err != nil {
|
||||
t.Fatal("Error on mysql creation: ", err.Error())
|
||||
return // No point in going further
|
||||
}
|
||||
}
|
||||
for _, tbl := range []string{utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA} {
|
||||
if _, err := mysql.Db.Query(fmt.Sprintf("SELECT 1 from %s", tbl)); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvInitDataDb(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
ratingDb, err := engine.ConfigureRatingStorage(fscsvCfg.RatingDBType, fscsvCfg.RatingDBHost, fscsvCfg.RatingDBPort, fscsvCfg.RatingDBName, fscsvCfg.RatingDBUser, fscsvCfg.RatingDBPass, fscsvCfg.DBDataEncoding)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to dataDb", err)
|
||||
}
|
||||
accountDb, err := engine.ConfigureAccountingStorage(fscsvCfg.AccountDBType, fscsvCfg.AccountDBHost, fscsvCfg.AccountDBPort, fscsvCfg.AccountDBName,
|
||||
fscsvCfg.AccountDBUser, fscsvCfg.AccountDBPass, fscsvCfg.DBDataEncoding)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to dataDb", err)
|
||||
}
|
||||
for _, db := range []engine.Storage{ratingDb, accountDb} {
|
||||
if err := db.Flush(); err != nil {
|
||||
t.Fatal("Cannot reset dataDb", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvStartFs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
exec.Command("pkill", "freeswitch").Run() // Just to make sure no freeswitch is running
|
||||
go func() {
|
||||
fs := exec.Command("sudo", "/usr/share/cgrates/tutorials/fs_csv/freeswitch/etc/init.d/freeswitch", "start")
|
||||
out, _ := fs.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsCsv: %s", out))
|
||||
}()
|
||||
time.Sleep(time.Duration(*waitFs) * time.Millisecond) // Give time to rater to fire up
|
||||
}
|
||||
|
||||
// Finds cgr-engine executable and starts it with default configuration
|
||||
func TestFsCsvStartEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
exec.Command("pkill", "cgr-engine").Run() // Just to make sure another one is not running, bit brutal maybe we can fine tune it
|
||||
go func() {
|
||||
eng := exec.Command("sudo", "/usr/share/cgrates/tutorials/fs_json/cgrates/etc/init.d/cgrates", "start")
|
||||
out, _ := eng.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsCsv: %s", out))
|
||||
}()
|
||||
time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time to rater to fire up
|
||||
}
|
||||
|
||||
// Connect rpc client to rater
|
||||
func TestFsCsvRpcConn(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
rater, err = jsonrpc.Dial("tcp", fscsvCfg.RPCJSONListen)
|
||||
if err != nil {
|
||||
t.Fatal("Could not connect to rater: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we start with fresh data
|
||||
func TestFsCsvEmptyCache(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{Destinations: 0, RatingPlans: 0, RatingProfiles: 0, Actions: 0}
|
||||
var args utils.AttrCacheStats
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvLoadTariffPlans(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
// Simple test that command is executed without errors
|
||||
attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tutorials", "fs_csv", "cgrates", "tariffplans")}
|
||||
if err := rater.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.LoadTariffPlanFromFolder: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.LoadTariffPlanFromFolder got reply: ", reply)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond) // Give time for scheduler to execute topups
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{Destinations: 3, RatingPlans: 1, RatingProfiles: 1, Actions: 2}
|
||||
var args utils.AttrCacheStats
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvGetAccount(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var reply *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 10.0 { // We expect 11.5 since we have added in the previous test 1.5
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 10.0, received: %f", reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvCall1(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
tStart := time.Date(2014, 01, 15, 6, 0, 0, 0, time.UTC)
|
||||
tEnd := time.Date(2014, 01, 15, 6, 0, 35, 0, time.UTC)
|
||||
cd := engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
TOR: "call",
|
||||
Tenant: "cgrates.org",
|
||||
Subject: "1001",
|
||||
Account: "1001",
|
||||
Destination: "1002",
|
||||
TimeStart: tStart,
|
||||
TimeEnd: tEnd,
|
||||
CallDuration: 35,
|
||||
}
|
||||
var cc engine.CallCost
|
||||
// Make sure the cost is what we expect it is
|
||||
if err := rater.Call("Responder.GetCost", cd, &cc); err != nil {
|
||||
t.Error("Got error on Responder.GetCost: ", err.Error())
|
||||
} else if cc.GetConnectFee() != 0.4 && cc.Cost != 0.6 {
|
||||
t.Errorf("Calling Responder.GetCost got callcost: %v", cc)
|
||||
}
|
||||
// Make sure debit charges what cost returned
|
||||
if err := rater.Call("Responder.MaxDebit", cd, &cc); err != nil {
|
||||
t.Error("Got error on Responder.MaxDebit: ", err.Error())
|
||||
} else if cc.GetConnectFee() != 0.4 && cc.Cost != 0.6 {
|
||||
t.Errorf("Calling Responder.MaxDebit got callcost: %v", cc)
|
||||
}
|
||||
// Make sure the account was debited correctly for the first loop index (ConnectFee included)
|
||||
var reply *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 9.4 { // We expect 11.5 since we have added in the previous test 1.5
|
||||
t.Errorf("Calling ApierV1.GetAccount expected: 9.4, received: %f", reply.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
} else if len(reply.UnitCounters) != 1 ||
|
||||
utils.Round(reply.UnitCounters[0].Balances[0].Value, 2, utils.ROUNDING_MIDDLE) != 0.6 { // Make sure we correctly count usage
|
||||
t.Errorf("Received unexpected UnitCounters: %v", reply.UnitCounters)
|
||||
}
|
||||
cd = engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
TOR: "call",
|
||||
Tenant: "cgrates.org",
|
||||
Subject: "1001",
|
||||
Account: "1001",
|
||||
Destination: "1002",
|
||||
TimeStart: tStart,
|
||||
TimeEnd: tEnd,
|
||||
CallDuration: 35,
|
||||
LoopIndex: 1, // Should not charge ConnectFee
|
||||
}
|
||||
// Make sure debit charges what cost returned
|
||||
if err := rater.Call("Responder.MaxDebit", cd, &cc); err != nil {
|
||||
t.Error("Got error on Responder.MaxDebit: ", err.Error())
|
||||
} else if cc.GetConnectFee() != 0.4 && cc.Cost != 0.2 { // Does not contain connectFee, however connectFee should be correctly reported
|
||||
t.Errorf("Calling Responder.MaxDebit got callcost: %v", cc)
|
||||
}
|
||||
// Make sure the account was debited correctly for the first loop index (ConnectFee included)
|
||||
var reply2 *engine.Account
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &reply2); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else if utils.Round(reply2.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue(), 2, utils.ROUNDING_MIDDLE) != 9.20 {
|
||||
t.Errorf("Calling ApierV1.GetAccount expected: 9.2, received: %f", reply2.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
} else if len(reply2.UnitCounters) != 1 ||
|
||||
utils.Round(reply2.UnitCounters[0].Balances[0].Value, 2, utils.ROUNDING_MIDDLE) != 0.8 { // Make sure we correctly count usage
|
||||
t.Errorf("Received unexpected UnitCounters: %v", reply2.UnitCounters)
|
||||
}
|
||||
}
|
||||
|
||||
// Simply kill the engine after we are done with tests within this file
|
||||
func TestFsCsvStopEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
eng := exec.Command("/usr/share/cgrates/tutorials/fs_csv/cgrates/etc/init.d/cgrates", "stop")
|
||||
out, _ := eng.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsCsv: %s", out))
|
||||
}()
|
||||
}
|
||||
|
||||
func TestFsCsvStopFs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
fs := exec.Command("/usr/share/cgrates/tutorials/fs_csv/freeswitch/etc/init.d/freeswitch", "stop")
|
||||
out, _ := fs.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsCsv: %s", out))
|
||||
}()
|
||||
}
|
||||
504
apier/tutfsjson_local_test.go
Normal file
504
apier/tutfsjson_local_test.go
Normal file
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package apier
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
var fsjsonCfgPath string
|
||||
var fsjsonCfg *config.CGRConfig
|
||||
|
||||
var waitFs = flag.Int("wait_fs", 500, "Number of miliseconds to wait for FreeSWITCH to start")
|
||||
|
||||
func init() {
|
||||
fsjsonCfgPath = path.Join(*dataDir, "tutorials", "fs_json", "cgrates", "etc", "cgrates", "cgrates.cfg")
|
||||
fsjsonCfg, _ = config.NewCGRConfig(&fsjsonCfgPath)
|
||||
}
|
||||
|
||||
// Remove here so they can be properly created by init script
|
||||
func TestFsJsonRemoveDirs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
for _, pathDir := range []string{fsjsonCfg.CdreDir, fsjsonCfg.HistoryDir} {
|
||||
if err := os.RemoveAll(pathDir); err != nil {
|
||||
t.Fatal("Error removing folder: ", pathDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty tables before using them
|
||||
func TestFsJsonCreateTables(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
if *storDbType != utils.MYSQL {
|
||||
t.Fatal("Unsupported storDbType")
|
||||
}
|
||||
var mysql *engine.MySQLStorage
|
||||
if d, err := engine.NewMySQLStorage(fsjsonCfg.StorDBHost, fsjsonCfg.StorDBPort, fsjsonCfg.StorDBName, fsjsonCfg.StorDBUser, fsjsonCfg.StorDBPass); err != nil {
|
||||
t.Fatal("Error on opening database connection: ", err)
|
||||
} else {
|
||||
mysql = d.(*engine.MySQLStorage)
|
||||
}
|
||||
for _, scriptName := range []string{engine.CREATE_CDRS_TABLES_SQL, engine.CREATE_COSTDETAILS_TABLES_SQL, engine.CREATE_MEDIATOR_TABLES_SQL} {
|
||||
if err := mysql.CreateTablesFromScript(path.Join(*dataDir, "storage", *storDbType, scriptName)); err != nil {
|
||||
t.Fatal("Error on mysql creation: ", err.Error())
|
||||
return // No point in going further
|
||||
}
|
||||
}
|
||||
for _, tbl := range []string{utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA} {
|
||||
if _, err := mysql.Db.Query(fmt.Sprintf("SELECT 1 from %s", tbl)); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonInitDataDb(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
ratingDb, err := engine.ConfigureRatingStorage(fsjsonCfg.RatingDBType, fsjsonCfg.RatingDBHost, fsjsonCfg.RatingDBPort, fsjsonCfg.RatingDBName, fsjsonCfg.RatingDBUser, fsjsonCfg.RatingDBPass, fsjsonCfg.DBDataEncoding)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to dataDb", err)
|
||||
}
|
||||
accountDb, err := engine.ConfigureAccountingStorage(fsjsonCfg.AccountDBType, fsjsonCfg.AccountDBHost, fsjsonCfg.AccountDBPort, fsjsonCfg.AccountDBName,
|
||||
fsjsonCfg.AccountDBUser, fsjsonCfg.AccountDBPass, fsjsonCfg.DBDataEncoding)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to dataDb", err)
|
||||
}
|
||||
for _, db := range []engine.Storage{ratingDb, accountDb} {
|
||||
if err := db.Flush(); err != nil {
|
||||
t.Fatal("Cannot reset dataDb", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonStartFs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
exec.Command("pkill", "freeswitch").Run() // Just to make sure another one is not running, bit brutal maybe we can fine tune it
|
||||
go func() {
|
||||
fs := exec.Command("/usr/share/cgrates/tutorials/fs_json/freeswitch/etc/init.d/freeswitch", "start")
|
||||
out, _ := fs.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsJson: %s", out))
|
||||
}()
|
||||
time.Sleep(time.Duration(*waitFs) * time.Millisecond) // Give time to rater to fire up
|
||||
}
|
||||
|
||||
// Finds cgr-engine executable and starts it with default configuration
|
||||
func TestFsJsonStartEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
exec.Command("pkill", "cgr-engine").Run() // Just to make sure another one is not running, bit brutal maybe we can fine tune it
|
||||
go func() {
|
||||
eng := exec.Command("/usr/share/cgrates/tutorials/fs_json/cgrates/etc/init.d/cgrates", "start")
|
||||
out, _ := eng.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsJson: %s", out))
|
||||
}()
|
||||
time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time to rater to fire up
|
||||
}
|
||||
|
||||
// Connect rpc client to rater
|
||||
func TestFsJsonRpcConn(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
rater, err = jsonrpc.Dial("tcp", fsjsonCfg.RPCJSONListen)
|
||||
if err != nil {
|
||||
t.Fatal("Could not connect to rater: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we start with fresh data
|
||||
func TestFsJsonEmptyCache(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{Destinations: 0, RatingPlans: 0, RatingProfiles: 0, Actions: 0}
|
||||
var args utils.AttrCacheStats
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonLoadTariffPlans(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
// Simple test that command is executed without errors
|
||||
attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tutorials", "fs_json", "cgrates", "tariffplans")}
|
||||
if err := rater.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.LoadTariffPlanFromFolder: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.LoadTariffPlanFromFolder got reply: ", reply)
|
||||
}
|
||||
time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time for scheduler to execute topups
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{Destinations: 3, RatingPlans: 2, RatingProfiles: 2, Actions: 5, SharedGroups: 1, RatingAliases: 1, AccountAliases: 1}
|
||||
var args utils.AttrCacheStats
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonGetAccount1001(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var acnt *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
}
|
||||
if acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 10.0 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 10.0, received: %f", acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
if len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]) != 2 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]))
|
||||
}
|
||||
blncLst := acnt.BalanceMap[attrs.BalanceType+attrs.Direction]
|
||||
for _, blnc := range blncLst {
|
||||
if len(blnc.SharedGroup) == 0 && blnc.Value != 5 {
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
} else if blnc.SharedGroup == "SHARED_A" && blnc.Value != 5 {
|
||||
t.Errorf("Unexpected value for shared balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonGetAccount1002(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var acnt *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1002", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
}
|
||||
if acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 10.0 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 10.0, received: %f", acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
if len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]) != 1 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]))
|
||||
}
|
||||
blnc := acnt.BalanceMap[attrs.BalanceType+attrs.Direction][0]
|
||||
if blnc.Value != 10 {
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonGetAccount1003(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var acnt *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1003", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
}
|
||||
if acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 10.0 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 10.0, received: %f", acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
if len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]) != 1 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]))
|
||||
}
|
||||
blnc := acnt.BalanceMap[attrs.BalanceType+attrs.Direction][0]
|
||||
if blnc.Value != 10 {
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonGetAccount1004(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var acnt *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1004", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
}
|
||||
if acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 10.0 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 10.0, received: %f", acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
if len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]) != 1 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]))
|
||||
}
|
||||
blnc := acnt.BalanceMap[attrs.BalanceType+attrs.Direction][0]
|
||||
if blnc.Value != 10 {
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonGetAccount1006(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var acnt *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1006", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err == nil {
|
||||
t.Error("Got no error when querying unexisting balance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsJsonGetAccount1007(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var acnt *engine.Account
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1007", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
}
|
||||
if acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 0 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 0, received: %f", acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
if len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]) != 1 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]))
|
||||
}
|
||||
blncLst := acnt.BalanceMap[attrs.BalanceType+attrs.Direction]
|
||||
for _, blnc := range blncLst {
|
||||
if len(blnc.SharedGroup) == 0 && blnc.Value != 0 { // General balance
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
} else if blnc.SharedGroup == "SHARED_A" && blnc.Value != 0 {
|
||||
t.Errorf("Unexpected value for shared balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxCallDuration(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
cd := engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
Tenant: "cgrates.org",
|
||||
TOR: "call",
|
||||
Subject: "1001",
|
||||
Account: "1001",
|
||||
Destination: "1002",
|
||||
TimeStart: time.Now(),
|
||||
TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration),
|
||||
}
|
||||
var remainingDurationFloat float64
|
||||
if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
remainingDuration := time.Duration(remainingDurationFloat)
|
||||
if remainingDuration < time.Duration(90)*time.Minute {
|
||||
t.Errorf("Expecting maxSessionTime around 1h30m, received as: %v", remainingDuration)
|
||||
}
|
||||
}
|
||||
cd = engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
Tenant: "cgrates.org",
|
||||
TOR: "call",
|
||||
Subject: "1002",
|
||||
Account: "1002",
|
||||
Destination: "1001",
|
||||
TimeStart: time.Now(),
|
||||
TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration),
|
||||
}
|
||||
if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
remainingDuration := time.Duration(remainingDurationFloat)
|
||||
if remainingDuration < time.Duration(45)*time.Minute {
|
||||
t.Errorf("Expecting maxSessionTime around 45m, received as: %v", remainingDuration)
|
||||
}
|
||||
}
|
||||
cd = engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
Tenant: "cgrates.org",
|
||||
TOR: "call",
|
||||
Subject: "1006",
|
||||
Account: "1006",
|
||||
Destination: "1001",
|
||||
TimeStart: time.Now(),
|
||||
TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration),
|
||||
}
|
||||
if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
remainingDuration := time.Duration(remainingDurationFloat)
|
||||
if remainingDuration < time.Duration(45)*time.Minute {
|
||||
t.Errorf("Expecting maxSessionTime around 45m, received as: %v", remainingDuration)
|
||||
}
|
||||
}
|
||||
// 1007 should use the 1001 balance when doing maxSessionTime
|
||||
cd = engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
Tenant: "cgrates.org",
|
||||
TOR: "call",
|
||||
Subject: "1007",
|
||||
Account: "1007",
|
||||
Destination: "1001",
|
||||
TimeStart: time.Now(),
|
||||
TimeEnd: time.Now().Add(fsjsonCfg.SMMaxCallDuration),
|
||||
}
|
||||
if err := rater.Call("Responder.GetMaxSessionTime", cd, &remainingDurationFloat); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
remainingDuration := time.Duration(remainingDurationFloat)
|
||||
if remainingDuration < time.Duration(20)*time.Minute {
|
||||
t.Errorf("Expecting maxSessionTime around 20m, received as: %v", remainingDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxDebit1001(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
cc := &engine.CallCost{}
|
||||
var acnt *engine.Account
|
||||
cd := engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
Tenant: "cgrates.org",
|
||||
TOR: "call",
|
||||
Subject: "1001",
|
||||
Account: "1001",
|
||||
Destination: "1002",
|
||||
TimeStart: time.Now(),
|
||||
TimeEnd: time.Now().Add(time.Duration(10) * time.Second),
|
||||
}
|
||||
if err := rater.Call("Responder.MaxDebit", cd, cc); err != nil {
|
||||
t.Error(err.Error())
|
||||
} else if cc.GetDuration() > time.Duration(1)*time.Minute {
|
||||
t.Errorf("Unexpected call duration received: %v", cc.GetDuration())
|
||||
}
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else {
|
||||
if len(acnt.BalanceMap["*monetary*out"]) != 2 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap["*monetary*out"]))
|
||||
}
|
||||
blncLst := acnt.BalanceMap["*monetary*out"]
|
||||
for _, blnc := range blncLst {
|
||||
if blnc.SharedGroup == "SHARED_A" && blnc.Value != 5 {
|
||||
t.Errorf("Unexpected value for shared balance: %f", blnc.Value)
|
||||
} else if len(blnc.SharedGroup) == 0 && blnc.Value != 4.4 {
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxDebit1007(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
cc := &engine.CallCost{}
|
||||
var acnt *engine.Account
|
||||
cd := engine.CallDescriptor{
|
||||
Direction: "*out",
|
||||
Tenant: "cgrates.org",
|
||||
TOR: "call",
|
||||
Subject: "1007",
|
||||
Account: "1007",
|
||||
Destination: "1002",
|
||||
TimeStart: time.Now(),
|
||||
TimeEnd: time.Now().Add(time.Duration(10) * time.Second),
|
||||
}
|
||||
if err := rater.Call("Responder.MaxDebit", cd, cc); err != nil {
|
||||
t.Error(err.Error())
|
||||
} else if cc.GetDuration() > time.Duration(1)*time.Minute {
|
||||
t.Errorf("Unexpected call duration received: %v", cc.GetDuration())
|
||||
}
|
||||
// Debit out of shared balance should reflect in the 1001 instead of 1007
|
||||
attrs := &AttrGetAccount{Tenant: "cgrates.org", Account: "1001", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
} else {
|
||||
if len(acnt.BalanceMap["*monetary*out"]) != 2 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap["*monetary*out"]))
|
||||
}
|
||||
blncLst := acnt.BalanceMap["*monetary*out"]
|
||||
for _, blnc := range blncLst {
|
||||
if blnc.SharedGroup == "SHARED_A" && blnc.Value != 4 {
|
||||
t.Errorf("Unexpected value for shared balance: %f", blnc.Value)
|
||||
} else if len(blnc.SharedGroup) == 0 && blnc.Value != 4.4 {
|
||||
t.Errorf("Unexpected value for general balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure 1007 remains the same
|
||||
attrs = &AttrGetAccount{Tenant: "cgrates.org", Account: "1007", BalanceType: "*monetary", Direction: "*out"}
|
||||
if err := rater.Call("ApierV1.GetAccount", attrs, &acnt); err != nil {
|
||||
t.Error("Got error on ApierV1.GetAccount: ", err.Error())
|
||||
}
|
||||
if acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue() != 0 {
|
||||
t.Errorf("Calling ApierV1.GetBalance expected: 0, received: %f", acnt.BalanceMap[attrs.BalanceType+attrs.Direction].GetTotalValue())
|
||||
}
|
||||
if len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]) != 1 {
|
||||
t.Errorf("Unexpected number of balances found: %d", len(acnt.BalanceMap[attrs.BalanceType+attrs.Direction]))
|
||||
}
|
||||
blnc := acnt.BalanceMap[attrs.BalanceType+attrs.Direction][0]
|
||||
if len(blnc.SharedGroup) == 0 { // General balance
|
||||
t.Errorf("Unexpected general balance: %f", blnc.Value)
|
||||
} else if blnc.SharedGroup == "SHARED_A" && blnc.Value != 0 {
|
||||
t.Errorf("Unexpected value for shared balance: %f", blnc.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Simply kill the engine after we are done with tests within this file
|
||||
func TestFsJsonStopEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
eng := exec.Command("/usr/share/cgrates/tutorials/fs_json/cgrates/etc/init.d/cgrates", "stop")
|
||||
out, _ := eng.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsJson: %s", out))
|
||||
}()
|
||||
}
|
||||
|
||||
func TestFsJsonStopFs(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
fs := exec.Command("/usr/share/cgrates/tutorials/fs_json/freeswitch/etc/init.d/freeswitch", "stop")
|
||||
out, _ := fs.CombinedOutput()
|
||||
engine.Logger.Info(fmt.Sprintf("CgrEngine-TestFsJson: %s", out))
|
||||
}()
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package apier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/cdrexporter"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.ExportedFileCdrs) error {
|
||||
var tStart, tEnd time.Time
|
||||
var err error
|
||||
cdrFormat := strings.ToLower(attr.CdrFormat)
|
||||
if !utils.IsSliceMember(utils.CdreCdrFormats, cdrFormat) {
|
||||
return fmt.Errorf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, "CdrFormat")
|
||||
}
|
||||
if len(attr.TimeStart) != 0 {
|
||||
if tStart, err = utils.ParseTimeDetectLayout(attr.TimeStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(attr.TimeEnd) != 0 {
|
||||
if tEnd, err = utils.ParseTimeDetectLayout(attr.TimeEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cdrs, err := self.CdrDb.GetRatedCdrs(tStart, tEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var fileName string
|
||||
if cdrFormat == utils.CDRE_CSV && len(cdrs) != 0 {
|
||||
fileName = path.Join(self.Config.CdreDir, fmt.Sprintf("cdrs_%d.csv", time.Now().Unix()))
|
||||
fileOut, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer fileOut.Close()
|
||||
}
|
||||
csvWriter := cdrexporter.NewCsvCdrWriter(fileOut, self.Config.RoundingDecimals, self.Config.CdreExtraFields)
|
||||
for _, cdr := range cdrs {
|
||||
if err := csvWriter.Write(cdr); err != nil {
|
||||
os.Remove(fileName)
|
||||
return err
|
||||
}
|
||||
}
|
||||
csvWriter.Close()
|
||||
if attr.RemoveFromDb {
|
||||
cgrIds := make([]string, len(cdrs))
|
||||
for idx, cdr := range cdrs {
|
||||
cgrIds[idx] = cdr.CgrId
|
||||
}
|
||||
if err := self.CdrDb.RemRatedCdrs(cgrIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
*reply = utils.ExportedFileCdrs{fileName, len(cdrs)}
|
||||
return nil
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package apier
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
|
||||
// Empty tables before using them
|
||||
func TestFsCsvCreateTables(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
if *storDbType != utils.MYSQL {
|
||||
t.Fatal("Unsupported storDbType")
|
||||
}
|
||||
var mysql *engine.MySQLStorage
|
||||
if d, err := engine.NewMySQLStorage(cfg.StorDBHost, cfg.StorDBPort, cfg.StorDBName, cfg.StorDBUser, cfg.StorDBPass); err != nil {
|
||||
t.Fatal("Error on opening database connection: ", err)
|
||||
} else {
|
||||
mysql = d.(*engine.MySQLStorage)
|
||||
}
|
||||
for _, scriptName := range []string{engine.CREATE_CDRS_TABLES_SQL, engine.CREATE_COSTDETAILS_TABLES_SQL, engine.CREATE_MEDIATOR_TABLES_SQL, engine.CREATE_TARIFFPLAN_TABLES_SQL} {
|
||||
if err := mysql.CreateTablesFromScript(path.Join(*dataDir, "storage", *storDbType, scriptName)); err != nil {
|
||||
t.Fatal("Error on mysql creation: ", err.Error())
|
||||
return // No point in going further
|
||||
}
|
||||
}
|
||||
for _, tbl := range []string{utils.TBL_CDRS_PRIMARY, utils.TBL_CDRS_EXTRA} {
|
||||
if _, err := mysql.Db.Query(fmt.Sprintf("SELECT 1 from %s", tbl)); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvInitDataDb(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
ratingDb, err := engine.ConfigureRatingStorage(cfg.RatingDBType, cfg.RatingDBHost, cfg.RatingDBPort, cfg.RatingDBName, cfg.RatingDBUser, cfg.RatingDBPass, cfg.DBDataEncoding)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to dataDb", err)
|
||||
}
|
||||
accountDb, err := engine.ConfigureAccountingStorage(cfg.AccountDBType, cfg.AccountDBHost, cfg.AccountDBPort, cfg.AccountDBName,
|
||||
cfg.AccountDBUser, cfg.AccountDBPass, cfg.DBDataEncoding)
|
||||
if err != nil {
|
||||
t.Fatal("Cannot connect to dataDb", err)
|
||||
}
|
||||
for _, db := range []engine.Storage{ratingDb, accountDb} {
|
||||
if err := db.Flush(); err != nil {
|
||||
t.Fatal("Cannot reset dataDb", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds cgr-engine executable and starts it with default configuration
|
||||
func TestFsCsvStartEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
enginePath, err := exec.LookPath("cgr-engine")
|
||||
if err != nil {
|
||||
t.Fatal("Cannot find cgr-engine executable")
|
||||
}
|
||||
exec.Command("pkill", "cgr-engine").Run() // Just to make sure another one is not running, bit brutal maybe we can fine tune it
|
||||
engine := exec.Command(enginePath, "-rater", "-scheduler", "-cdrs", "-mediator", "-config", path.Join(*dataDir, "conf", "cgrates.cfg"))
|
||||
if err := engine.Start(); err != nil {
|
||||
t.Fatal("Cannot start cgr-engine: ", err.Error())
|
||||
}
|
||||
time.Sleep(time.Duration(*waitRater) * time.Millisecond) // Give time to rater to fire up
|
||||
}
|
||||
|
||||
// Connect rpc client to rater
|
||||
func TestFsCsvRpcConn(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if cfg.RPCEncoding == utils.JSON {
|
||||
rater, err = jsonrpc.Dial("tcp", cfg.MediatorRater)
|
||||
} else {
|
||||
rater, err = rpc.Dial("tcp", cfg.MediatorRater)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal("Could not connect to rater: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make sure we start with fresh data
|
||||
func TestFsCsvEmptyCache(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
arc := new(utils.ApiReloadCache)
|
||||
// Simple test that command is executed without errors
|
||||
if err := rater.Call("ApierV1.ReloadCache", arc, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.ReloadCache: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.ReloadCache got reply: ", reply)
|
||||
}
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{Destinations: 0, RatingPlans: 0, RatingProfiles: 0, Actions: 0}
|
||||
var args utils.AttrCacheStats
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvLoadTariffPlans(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
reply := ""
|
||||
// Simple test that command is executed without errors
|
||||
attrs := &AttrLoadTPFromFolder{FolderPath: path.Join(*dataDir, "tutorials", "fs_csv", "cgrates", "tariffplans")}
|
||||
if err := rater.Call("ApierV1.LoadTariffPlanFromFolder", attrs, &reply); err != nil {
|
||||
t.Error("Got error on ApierV1.LoadTariffPlanFromFolder: ", err.Error())
|
||||
} else if reply != "OK" {
|
||||
t.Error("Calling ApierV1.LoadTariffPlanFromFolder got reply: ", reply)
|
||||
}
|
||||
var rcvStats *utils.CacheStats
|
||||
expectedStats := &utils.CacheStats{Destinations: 3, RatingPlans: 1, RatingProfiles: 1, Actions: 2}
|
||||
var args utils.AttrCacheStats
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsCsvCall1(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
tStart := time.Date(2014, 01, 15, 6, 0, 0, 0, time.UTC)
|
||||
tEnd := time.Date(2014, 01, 15, 6, 0, 35, 0, time.UTC)
|
||||
cd := engine.CallDescriptor {
|
||||
Direction: "*out",
|
||||
TOR: "call",
|
||||
Tenant: "cgrates.org",
|
||||
Subject: "1001",
|
||||
Account: "1001",
|
||||
Destination: "1002",
|
||||
TimeStart: tStart,
|
||||
TimeEnd: tEnd,
|
||||
CallDuration: 35,
|
||||
}
|
||||
var cc engine.CallCost
|
||||
// Simple test that command is executed without errors
|
||||
if err := rater.Call("Responder.GetCost", cd, &cc); err != nil {
|
||||
t.Error("Got error on Responder.GetCost: ", err.Error())
|
||||
} else if cc.ConnectFee != 0.4 && cc.Cost != 0.2 {
|
||||
t.Errorf("Calling Responder.GetCost got callcost: %v", cc)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Simply kill the engine after we are done with tests within this file
|
||||
func TestFsCsvStopEngine(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
exec.Command("pkill", "cgr-engine").Run()
|
||||
}
|
||||
102
cdrc/cdrc.go
102
cdrc/cdrc.go
@@ -23,19 +23,20 @@ import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"github.com/howeyc/fsnotify"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/cdrs"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"github.com/howeyc/fsnotify"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,8 +44,8 @@ const (
|
||||
FS_CSV = "freeswitch_csv"
|
||||
)
|
||||
|
||||
func NewCdrc(config *config.CGRConfig) (*Cdrc, error) {
|
||||
cdrc := &Cdrc{cgrCfg: config}
|
||||
func NewCdrc(config *config.CGRConfig, cdrServer *cdrs.CDRS) (*Cdrc, error) {
|
||||
cdrc := &Cdrc{cgrCfg: config, cdrServer: cdrServer}
|
||||
// Before processing, make sure in and out folders exist
|
||||
for _, dir := range []string{cdrc.cgrCfg.CdrcCdrInDir, cdrc.cgrCfg.CdrcCdrOutDir} {
|
||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||
@@ -59,9 +60,10 @@ func NewCdrc(config *config.CGRConfig) (*Cdrc, error) {
|
||||
}
|
||||
|
||||
type Cdrc struct {
|
||||
cgrCfg *config.CGRConfig
|
||||
cgrCfg *config.CGRConfig
|
||||
cdrServer *cdrs.CDRS
|
||||
cfgCdrFields map[string]string // Key is the name of the field
|
||||
httpClient *http.Client
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// When called fires up folder monitoring, either automated via inotify or manual by sleeping between processing
|
||||
@@ -80,17 +82,18 @@ func (self *Cdrc) Run() error {
|
||||
func (self *Cdrc) parseFieldsConfig() error {
|
||||
var err error
|
||||
self.cfgCdrFields = map[string]string{
|
||||
utils.ACCID: self.cgrCfg.CdrcAccIdField,
|
||||
utils.REQTYPE: self.cgrCfg.CdrcReqTypeField,
|
||||
utils.DIRECTION: self.cgrCfg.CdrcDirectionField,
|
||||
utils.TENANT: self.cgrCfg.CdrcTenantField,
|
||||
utils.TOR: self.cgrCfg.CdrcTorField,
|
||||
utils.ACCOUNT: self.cgrCfg.CdrcAccountField,
|
||||
utils.SUBJECT: self.cgrCfg.CdrcSubjectField,
|
||||
utils.DESTINATION: self.cgrCfg.CdrcDestinationField,
|
||||
utils.ANSWER_TIME: self.cgrCfg.CdrcAnswerTimeField,
|
||||
utils.DURATION: self.cgrCfg.CdrcDurationField,
|
||||
}
|
||||
utils.ACCID: self.cgrCfg.CdrcAccIdField,
|
||||
utils.REQTYPE: self.cgrCfg.CdrcReqTypeField,
|
||||
utils.DIRECTION: self.cgrCfg.CdrcDirectionField,
|
||||
utils.TENANT: self.cgrCfg.CdrcTenantField,
|
||||
utils.TOR: self.cgrCfg.CdrcTorField,
|
||||
utils.ACCOUNT: self.cgrCfg.CdrcAccountField,
|
||||
utils.SUBJECT: self.cgrCfg.CdrcSubjectField,
|
||||
utils.DESTINATION: self.cgrCfg.CdrcDestinationField,
|
||||
utils.SETUP_TIME: self.cgrCfg.CdrcSetupTimeField,
|
||||
utils.ANSWER_TIME: self.cgrCfg.CdrcAnswerTimeField,
|
||||
utils.DURATION: self.cgrCfg.CdrcDurationField,
|
||||
}
|
||||
|
||||
// Add extra fields here, config extra fields in the form of []string{"fieldName1:indxInCsv1","fieldName2: indexInCsv2"}
|
||||
for _, fieldWithIdx := range self.cgrCfg.CdrcExtraFields {
|
||||
@@ -115,10 +118,9 @@ func (self *Cdrc) parseFieldsConfig() error {
|
||||
}
|
||||
|
||||
// Takes the record out of csv and turns it into http form which can be posted
|
||||
func (self *Cdrc) cdrAsHttpForm(record []string) (url.Values, error) {
|
||||
// engine.Logger.Info(fmt.Sprintf("Processing record %v", record))
|
||||
v := url.Values{}
|
||||
v.Set(utils.CDRSOURCE, self.cgrCfg.CdrcSourceId)
|
||||
func (self *Cdrc) recordAsStoredCdr(record []string) (*utils.StoredCdr, error) {
|
||||
ratedCdr := &utils.StoredCdr{CdrSource: self.cgrCfg.CdrcSourceId, ExtraFields: map[string]string{}, Cost: -1}
|
||||
var err error
|
||||
for cfgFieldName, cfgFieldVal := range self.cfgCdrFields {
|
||||
var fieldVal string
|
||||
if strings.HasPrefix(cfgFieldVal, utils.STATIC_VALUE_PREFIX) {
|
||||
@@ -134,9 +136,42 @@ func (self *Cdrc) cdrAsHttpForm(record []string) (url.Values, error) {
|
||||
} else { // Modify here when we add more supported cdr formats
|
||||
fieldVal = "UNKNOWN"
|
||||
}
|
||||
v.Set(cfgFieldName, fieldVal)
|
||||
switch cfgFieldName {
|
||||
case utils.ACCID:
|
||||
ratedCdr.CgrId = utils.FSCgrId(fieldVal)
|
||||
ratedCdr.AccId = fieldVal
|
||||
case utils.REQTYPE:
|
||||
ratedCdr.ReqType = fieldVal
|
||||
case utils.DIRECTION:
|
||||
ratedCdr.Direction = fieldVal
|
||||
case utils.TENANT:
|
||||
ratedCdr.Tenant = fieldVal
|
||||
case utils.TOR:
|
||||
ratedCdr.TOR = fieldVal
|
||||
case utils.ACCOUNT:
|
||||
ratedCdr.Account = fieldVal
|
||||
case utils.SUBJECT:
|
||||
ratedCdr.Subject = fieldVal
|
||||
case utils.DESTINATION:
|
||||
ratedCdr.Destination = fieldVal
|
||||
case utils.SETUP_TIME:
|
||||
if ratedCdr.SetupTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse answer time field, err: %s", err.Error())
|
||||
}
|
||||
case utils.ANSWER_TIME:
|
||||
if ratedCdr.AnswerTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse answer time field, err: %s", err.Error())
|
||||
}
|
||||
case utils.DURATION:
|
||||
if ratedCdr.Duration, err = utils.ParseDurationWithSecs(fieldVal); err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse duration field, err: %s", err.Error())
|
||||
}
|
||||
default: // Extra fields will not match predefined so they all show up here
|
||||
ratedCdr.ExtraFields[cfgFieldName] = fieldVal
|
||||
}
|
||||
|
||||
}
|
||||
return v, nil
|
||||
return ratedCdr, nil
|
||||
}
|
||||
|
||||
// One run over the CDR folder
|
||||
@@ -198,14 +233,21 @@ func (self *Cdrc) processFile(filePath string) error {
|
||||
engine.Logger.Err(fmt.Sprintf("<Cdrc> Error in csv file: %s", err.Error()))
|
||||
continue // Other csv related errors, ignore
|
||||
}
|
||||
cdrAsForm, err := self.cdrAsHttpForm(record)
|
||||
rawCdr, err := self.recordAsStoredCdr(record)
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<Cdrc> Error in csv file: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
if _, err := self.httpClient.PostForm(fmt.Sprintf("http://%s/cgr", self.cgrCfg.CdrcCdrs), cdrAsForm); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<Cdrc> Failed posting CDR, error: %s", err.Error()))
|
||||
continue
|
||||
if self.cgrCfg.CdrcCdrs == utils.INTERNAL {
|
||||
if err := self.cdrServer.ProcessRawCdr(rawCdr); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<Cdrc> Failed posting CDR, error: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
} else { // CDRs listening on IP
|
||||
if _, err := self.httpClient.PostForm(fmt.Sprintf("http://%s/cgr", self.cgrCfg.HTTPListen), rawCdr.AsRawCdrHttpForm()); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<Cdrc> Failed posting CDR, error: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finished with file, move it to processed folder
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -45,6 +46,7 @@ README:
|
||||
*
|
||||
*/
|
||||
|
||||
var cfgPath string
|
||||
var cfg *config.CGRConfig
|
||||
|
||||
var testLocal = flag.Bool("local", false, "Perform the tests only on local test environment, not by default.") // This flag will be passed here via "go test -local" args
|
||||
@@ -53,7 +55,7 @@ var storDbType = flag.String("stordb_type", "mysql", "The type of the storDb dat
|
||||
var waitRater = flag.Int("wait_rater", 300, "Number of miliseconds to wait for rater to start and cache")
|
||||
|
||||
func init() {
|
||||
cfgPath := path.Join(*dataDir, "conf", "cgrates.cfg")
|
||||
cfgPath = path.Join(*dataDir, "conf", "samples", "apier_local_test.cfg")
|
||||
cfg, _ = config.NewCGRConfig(&cfgPath)
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ func startEngine() error {
|
||||
return errors.New("Cannot find cgr-engine executable")
|
||||
}
|
||||
stopEngine()
|
||||
engine := exec.Command(enginePath, "-cdrs", "-config", path.Join(*dataDir, "conf", "cgrates.cfg"))
|
||||
engine := exec.Command(enginePath, "-cdrs", "-config", cfgPath)
|
||||
if err := engine.Start(); err != nil {
|
||||
return fmt.Errorf("Cannot start cgr-engine: %s", err.Error())
|
||||
}
|
||||
@@ -118,6 +120,12 @@ func TestCreateCdrFiles(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(cfg.CdrcCdrInDir); err != nil {
|
||||
t.Fatal("Error removing folder: ", cfg.CdrcCdrInDir, err)
|
||||
}
|
||||
if err := os.MkdirAll(cfg.CdrcCdrInDir, 0755); err != nil {
|
||||
t.Fatal("Error creating folder: ", cfg.CdrcCdrInDir, err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(cfg.CdrcCdrInDir, "file1.csv"), []byte(fileContent1), 0644); err != nil {
|
||||
t.Fatal(err.Error)
|
||||
}
|
||||
@@ -130,10 +138,13 @@ func TestProcessCdrDir(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
if cfg.CdrcCdrs == utils.INTERNAL { // For now we only test over network
|
||||
return
|
||||
}
|
||||
if err := startEngine(); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
cdrc, err := NewCdrc(cfg)
|
||||
cdrc, err := NewCdrc(cfg, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ package cdrc
|
||||
import (
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseFieldsConfig(t *testing.T) {
|
||||
@@ -50,33 +52,57 @@ func TestParseFieldsConfig(t *testing.T) {
|
||||
cgrConfig.CdrcExtraFields = []string{"supplier1:^top_supplier", "orig_ip:11"}
|
||||
cdrc = &Cdrc{cgrCfg: cgrConfig}
|
||||
if err := cdrc.parseFieldsConfig(); err != nil {
|
||||
t.Errorf("Failed to corectly parse extra fields %v",cdrc.cfgCdrFields)
|
||||
t.Errorf("Failed to corectly parse extra fields %v", cdrc.cfgCdrFields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdrAsHttpForm(t *testing.T) {
|
||||
func TestRecordAsStoredCdr(t *testing.T) {
|
||||
cgrConfig, _ := config.NewDefaultCGRConfig()
|
||||
cgrConfig.CdrcExtraFields = []string{"supplier:11"}
|
||||
cdrc := &Cdrc{cgrCfg: cgrConfig}
|
||||
if err := cdrc.parseFieldsConfig(); err != nil {
|
||||
t.Error("Failed parsing default fieldIndexesFromConfig", err)
|
||||
}
|
||||
cdrRow := []string{"firstField", "secondField"}
|
||||
_, err := cdrc.cdrAsHttpForm(cdrRow)
|
||||
_, err := cdrc.recordAsStoredCdr(cdrRow)
|
||||
if err == nil {
|
||||
t.Error("Failed to corectly detect missing fields from record")
|
||||
}
|
||||
cdrRow = []string{"acc1", "prepaid", "*out", "cgrates.org", "call", "1001", "1001", "+4986517174963", "2013-02-03 19:54:00", "62", "supplier1", "172.16.1.1"}
|
||||
cdrAsForm, err := cdrc.cdrAsHttpForm(cdrRow)
|
||||
cdrRow = []string{"acc1", "prepaid", "*out", "cgrates.org", "call", "1001", "1001", "+4986517174963", "2013-02-03 19:50:00", "2013-02-03 19:54:00", "62",
|
||||
"supplier1", "172.16.1.1"}
|
||||
rtCdr, err := cdrc.recordAsStoredCdr(cdrRow)
|
||||
if err != nil {
|
||||
t.Error("Failed to parse CDR in form", err)
|
||||
t.Error("Failed to parse CDR in rated cdr", err)
|
||||
}
|
||||
if cdrAsForm.Get(utils.CDRSOURCE) != cgrConfig.CdrcSourceId {
|
||||
t.Error("Unexpected cdrsource received", cdrAsForm.Get(utils.CDRSOURCE))
|
||||
expectedCdr := &utils.StoredCdr{
|
||||
CgrId: utils.FSCgrId(cdrRow[0]),
|
||||
AccId: cdrRow[0],
|
||||
CdrSource: cgrConfig.CdrcSourceId,
|
||||
ReqType: cdrRow[1],
|
||||
Direction: cdrRow[2],
|
||||
Tenant: cdrRow[3],
|
||||
TOR: cdrRow[4],
|
||||
Account: cdrRow[5],
|
||||
Subject: cdrRow[6],
|
||||
Destination: cdrRow[7],
|
||||
SetupTime: time.Date(2013, 2, 3, 19, 50, 0, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 2, 3, 19, 54, 0, 0, time.UTC),
|
||||
Duration: time.Duration(62) * time.Second,
|
||||
ExtraFields: map[string]string{"supplier": "supplier1"},
|
||||
Cost: -1,
|
||||
}
|
||||
if cdrAsForm.Get(utils.REQTYPE) != "prepaid" {
|
||||
t.Error("Unexpected CDR value received", cdrAsForm.Get(utils.REQTYPE))
|
||||
if !reflect.DeepEqual(expectedCdr, rtCdr) {
|
||||
t.Errorf("Expected: \n%v, \nreceived: \n%v", expectedCdr, rtCdr)
|
||||
}
|
||||
//if cdrAsForm.Get("supplier") != "supplier1" {
|
||||
// t.Error("Unexpected CDR value received", cdrAsForm.Get("supplier"))
|
||||
//}
|
||||
/*
|
||||
if cdrAsForm.Get(utils.CDRSOURCE) != cgrConfig.CdrcSourceId {
|
||||
t.Error("Unexpected cdrsource received", cdrAsForm.Get(utils.CDRSOURCE))
|
||||
}
|
||||
if cdrAsForm.Get(utils.REQTYPE) != "prepaid" {
|
||||
t.Error("Unexpected CDR value received", cdrAsForm.Get(utils.REQTYPE))
|
||||
}
|
||||
if cdrAsForm.Get("supplier") != "supplier1" {
|
||||
t.Error("Unexpected CDR value received", cdrAsForm.Get("supplier"))
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdrexporter
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
type CdrWriter interface {
|
||||
Write(cdr utils.RatedCDR) string
|
||||
WriteCdr(cdr *utils.StoredCdr) string
|
||||
Close()
|
||||
}
|
||||
53
cdre/csv.go
Normal file
53
cdre/csv.go
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io"
|
||||
)
|
||||
|
||||
type CsvCdrWriter struct {
|
||||
writer *csv.Writer
|
||||
roundDecimals int // Round floats like Cost using this number of decimals
|
||||
exportedFields []*utils.RSRField // The fields exported, order important
|
||||
}
|
||||
|
||||
func NewCsvCdrWriter(writer io.Writer, roundDecimals int, exportedFields []*utils.RSRField) *CsvCdrWriter {
|
||||
return &CsvCdrWriter{csv.NewWriter(writer), roundDecimals, exportedFields}
|
||||
}
|
||||
|
||||
func (csvwr *CsvCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
|
||||
row := make([]string, len(csvwr.exportedFields))
|
||||
for idx, fld := range csvwr.exportedFields {
|
||||
var fldVal string
|
||||
if fld.Id == utils.COST {
|
||||
fldVal = cdr.FormatCost(csvwr.roundDecimals)
|
||||
} else {
|
||||
fldVal = cdr.ExportFieldValue(fld.Id)
|
||||
}
|
||||
row[idx] = fld.ParseValue(fldVal)
|
||||
}
|
||||
return csvwr.writer.Write(row)
|
||||
}
|
||||
|
||||
func (csvwr *CsvCdrWriter) Close() {
|
||||
csvwr.writer.Flush()
|
||||
}
|
||||
@@ -16,10 +16,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdrexporter
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -28,16 +29,19 @@ import (
|
||||
|
||||
func TestCsvCdrWriter(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
csvCdrWriter := NewCsvCdrWriter(writer, 4, []string{"extra3", "extra1"})
|
||||
ratedCdr := &utils.RatedCDR{CgrId: utils.FSCgrId("dsafdsaf"), AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
|
||||
TOR: "call", Account: "1001", Subject: "1001", Destination: "1002", AnswerTime: time.Unix(1383813746, 0).UTC(), Duration: 10, MediationRunId: utils.DEFAULT_RUNID,
|
||||
cfg, _ := config.NewDefaultCGRConfig()
|
||||
exportedFields := append(cfg.CdreExportedFields, &utils.RSRField{Id: "extra3"}, &utils.RSRField{Id: "dummy_extra"}, &utils.RSRField{Id: "extra1"})
|
||||
csvCdrWriter := NewCsvCdrWriter(writer, 4, exportedFields)
|
||||
ratedCdr := &utils.StoredCdr{CgrId: utils.FSCgrId("dsafdsaf"), AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
|
||||
TOR: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Unix(1383813745, 0).UTC(), AnswerTime: time.Unix(1383813746, 0).UTC(),
|
||||
Duration: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID,
|
||||
ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01,
|
||||
}
|
||||
csvCdrWriter.Write(ratedCdr)
|
||||
csvCdrWriter.WriteCdr(ratedCdr)
|
||||
csvCdrWriter.Close()
|
||||
expected := "b18944ef4dc618569f24c27b9872827a242bad0c,default,dsafdsaf,192.168.1.1,rated,*out,cgrates.org,call,1001,1001,1002,2013-11-07 08:42:26 +0000 UTC,10,1.0100,val_extra3,val_extra1"
|
||||
expected := `b18944ef4dc618569f24c27b9872827a242bad0c,default,dsafdsaf,192.168.1.1,rated,*out,cgrates.org,call,1001,1001,1002,2013-11-07 08:42:25 +0000 UTC,2013-11-07 08:42:26 +0000 UTC,10,1.0100,val_extra3,"",val_extra1`
|
||||
result := strings.TrimSpace(writer.String())
|
||||
if result != expected {
|
||||
t.Errorf("Expected %s received %s.", expected, result)
|
||||
t.Errorf("Expected: \n%s received: \n%s.", expected, result)
|
||||
}
|
||||
}
|
||||
267
cdre/fixedwidth.go
Normal file
267
cdre/fixedwidth.go
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
COST_DETAILS = "cost_details"
|
||||
FILLER = "filler"
|
||||
CONSTANT = "constant"
|
||||
CDRFIELD = "cdrfield"
|
||||
METATAG = "metatag"
|
||||
CONCATENATED_CDRFIELD = "concatenated_cdrfield"
|
||||
META_EXPORTID = "export_id"
|
||||
META_TIMENOW = "time_now"
|
||||
META_FIRSTCDRTIME = "first_cdr_time"
|
||||
META_LASTCDRTIME = "last_cdr_time"
|
||||
META_NRCDRS = "cdrs_number"
|
||||
META_DURCDRS = "cdrs_duration"
|
||||
META_COSTCDRS = "cdrs_cost"
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
func NewFWCdrWriter(logDb engine.LogStorage, outFile *os.File, exportTpl *config.CgrXmlCdreFwCfg, exportId string, roundDecimals int) (*FixedWidthCdrWriter, error) {
|
||||
return &FixedWidthCdrWriter{
|
||||
logDb: logDb,
|
||||
writer: outFile,
|
||||
exportTemplate: exportTpl,
|
||||
exportId: exportId,
|
||||
roundDecimals: roundDecimals,
|
||||
header: &bytes.Buffer{},
|
||||
content: &bytes.Buffer{},
|
||||
trailer: &bytes.Buffer{}}, nil
|
||||
}
|
||||
|
||||
type FixedWidthCdrWriter struct {
|
||||
logDb engine.LogStorage // Used to extract cost_details if these are requested
|
||||
writer io.Writer
|
||||
exportTemplate *config.CgrXmlCdreFwCfg
|
||||
exportId string // Unique identifier or this export
|
||||
roundDecimals int
|
||||
header, content, trailer *bytes.Buffer
|
||||
firstCdrTime, lastCdrTime time.Time
|
||||
numberOfRecords int
|
||||
totalDuration time.Duration
|
||||
totalCost float64
|
||||
}
|
||||
|
||||
// Return Json marshaled callCost attached to
|
||||
// Keep it separately so we test only this part in local tests
|
||||
func (fww *FixedWidthCdrWriter) getCdrCostDetails(cgrId, runId string) (string, error) {
|
||||
cc, err := fww.logDb.GetCallCostLog(cgrId, "", runId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if cc == nil {
|
||||
return "", nil
|
||||
}
|
||||
ccJson, _ := json.Marshal(cc)
|
||||
return string(ccJson), nil
|
||||
}
|
||||
|
||||
// Extracts the value specified by cfgHdr out of cdr
|
||||
func (fww *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layout string) (string, error) {
|
||||
rsrField, err := utils.NewRSRField(cfgHdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if rsrField == nil {
|
||||
return "", nil
|
||||
}
|
||||
var cdrVal string
|
||||
switch rsrField.Id {
|
||||
case COST_DETAILS: // Special case when we need to further extract cost_details out of logDb
|
||||
if cdrVal, err = fww.getCdrCostDetails(cdr.CgrId, cdr.MediationRunId); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case utils.COST:
|
||||
cdrVal = cdr.FormatCost(fww.roundDecimals)
|
||||
case utils.SETUP_TIME:
|
||||
cdrVal = cdr.SetupTime.Format(layout)
|
||||
case utils.ANSWER_TIME: // Format time based on layout
|
||||
cdrVal = cdr.AnswerTime.Format(layout)
|
||||
default:
|
||||
cdrVal = cdr.ExportFieldValue(rsrField.Id)
|
||||
}
|
||||
return rsrField.ParseValue(cdrVal), nil
|
||||
}
|
||||
|
||||
func (fww *FixedWidthCdrWriter) metaHandler(tag, layout string) (string, error) {
|
||||
switch tag {
|
||||
case META_EXPORTID:
|
||||
return fww.exportId, nil
|
||||
case META_TIMENOW:
|
||||
return time.Now().Format(layout), nil
|
||||
case META_FIRSTCDRTIME:
|
||||
return fww.firstCdrTime.Format(layout), nil
|
||||
case META_LASTCDRTIME:
|
||||
return fww.lastCdrTime.Format(layout), nil
|
||||
case META_NRCDRS:
|
||||
return strconv.Itoa(fww.numberOfRecords), nil
|
||||
case META_DURCDRS:
|
||||
return strconv.FormatFloat(fww.totalDuration.Seconds(), 'f', -1, 64), nil
|
||||
case META_COSTCDRS:
|
||||
return strconv.FormatFloat(utils.Round(fww.totalCost, fww.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil
|
||||
default:
|
||||
return "", errors.New("Unsupported METATAG")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Writes the header into it's buffer
|
||||
func (fww *FixedWidthCdrWriter) ComposeHeader() error {
|
||||
header := ""
|
||||
for _, cfgFld := range fww.exportTemplate.Header.Fields {
|
||||
var outVal string
|
||||
switch cfgFld.Type {
|
||||
case FILLER, CONSTANT:
|
||||
outVal = cfgFld.Value
|
||||
case METATAG:
|
||||
outVal, err = fww.metaHandler(cfgFld.Value, cfgFld.Layout)
|
||||
default:
|
||||
return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
|
||||
}
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR header, error: %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR header, error: %s", err.Error()))
|
||||
return err
|
||||
} else {
|
||||
header += fmtOut
|
||||
}
|
||||
}
|
||||
if len(header) == 0 { // No header data, most likely no configuration fields defined
|
||||
return nil
|
||||
}
|
||||
header += "\n" // Done with cdr, postpend new line char
|
||||
fww.header.WriteString(header)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Writes the trailer into it's buffer
|
||||
func (fww *FixedWidthCdrWriter) ComposeTrailer() error {
|
||||
trailer := ""
|
||||
for _, cfgFld := range fww.exportTemplate.Trailer.Fields {
|
||||
var outVal string
|
||||
switch cfgFld.Type {
|
||||
case FILLER, CONSTANT:
|
||||
outVal = cfgFld.Value
|
||||
case METATAG:
|
||||
outVal, err = fww.metaHandler(cfgFld.Value, cfgFld.Layout)
|
||||
default:
|
||||
return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
|
||||
}
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR trailer, error: %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR trailer, error: %s", err.Error()))
|
||||
return err
|
||||
} else {
|
||||
trailer += fmtOut
|
||||
}
|
||||
}
|
||||
if len(trailer) == 0 { // No header data, most likely no configuration fields defined
|
||||
return nil
|
||||
}
|
||||
trailer += "\n" // Done with cdr, postpend new line char
|
||||
fww.trailer.WriteString(trailer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write individual cdr into content buffer, build stats
|
||||
func (fww *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
|
||||
if cdr == nil || len(cdr.CgrId) == 0 { // We do not export empty CDRs
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
cdrRow := ""
|
||||
for _, cfgFld := range fww.exportTemplate.Content.Fields {
|
||||
var outVal string
|
||||
switch cfgFld.Type {
|
||||
case FILLER, CONSTANT:
|
||||
outVal = cfgFld.Value
|
||||
case CDRFIELD:
|
||||
outVal, err = fww.cdrFieldValue(cdr, cfgFld.Value, cfgFld.Layout)
|
||||
case CONCATENATED_CDRFIELD:
|
||||
for _, fld := range strings.Split(cfgFld.Value, ",") {
|
||||
if fldOut, err := fww.cdrFieldValue(cdr, fld, cfgFld.Layout); err != nil {
|
||||
break // The error will be reported bellow
|
||||
} else {
|
||||
outVal += fldOut
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error()))
|
||||
return err
|
||||
}
|
||||
if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<CdreFw> Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error()))
|
||||
return err
|
||||
} else {
|
||||
cdrRow += fmtOut
|
||||
}
|
||||
}
|
||||
if len(cdrRow) == 0 { // No CDR data, most likely no configuration fields defined
|
||||
return nil
|
||||
}
|
||||
cdrRow += "\n" // Done with cdr, postpend new line char
|
||||
fww.content.WriteString(cdrRow)
|
||||
// Done with writing content, compute stats here
|
||||
if fww.firstCdrTime.IsZero() || cdr.SetupTime.Before(fww.firstCdrTime) {
|
||||
fww.firstCdrTime = cdr.SetupTime
|
||||
}
|
||||
if cdr.SetupTime.After(fww.lastCdrTime) {
|
||||
fww.lastCdrTime = cdr.SetupTime
|
||||
}
|
||||
fww.numberOfRecords += 1
|
||||
fww.totalDuration += cdr.Duration
|
||||
fww.totalCost += cdr.Cost
|
||||
fww.totalCost = utils.Round(fww.totalCost, fww.roundDecimals, utils.ROUNDING_MIDDLE)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fww *FixedWidthCdrWriter) Close() {
|
||||
if fww.exportTemplate.Header != nil {
|
||||
fww.ComposeHeader()
|
||||
}
|
||||
if fww.exportTemplate.Trailer != nil {
|
||||
fww.ComposeTrailer()
|
||||
}
|
||||
for _, buf := range []*bytes.Buffer{fww.header, fww.content, fww.trailer} {
|
||||
fww.writer.Write(buf.Bytes())
|
||||
}
|
||||
}
|
||||
190
cdre/fixedwidth_test.go
Normal file
190
cdre/fixedwidth_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var hdrCfgFlds = []*config.CgrXmlCfgCdrField{
|
||||
&config.CgrXmlCfgCdrField{Name: "TypeOfRecord", Type: CONSTANT, Value: "10", Width: 2},
|
||||
&config.CgrXmlCfgCdrField{Name: "Filler1", Type: FILLER, Width: 3, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "DistributorCode", Type: CONSTANT, Value: "VOI", Width: 3},
|
||||
&config.CgrXmlCfgCdrField{Name: "FileSeqNr", Type: METATAG, Value: "export_id", Width: 5, Strip: "right", Padding: "zeroleft"},
|
||||
&config.CgrXmlCfgCdrField{Name: "LastCdr", Type: METATAG, Value: "last_cdr_time", Width: 12, Layout: "020106150400"},
|
||||
&config.CgrXmlCfgCdrField{Name: "FileCreationfTime", Type: METATAG, Value: "time_now", Width: 12, Layout: "020106150400"},
|
||||
&config.CgrXmlCfgCdrField{Name: "FileVersion", Type: CONSTANT, Value: "01", Width: 2},
|
||||
&config.CgrXmlCfgCdrField{Name: "Filler2", Type: FILLER, Width: 105, Padding: "right"},
|
||||
}
|
||||
|
||||
var contentCfgFlds = []*config.CgrXmlCfgCdrField{
|
||||
&config.CgrXmlCfgCdrField{Name: "TypeOfRecord", Type: CONSTANT, Value: "20", Width: 2},
|
||||
&config.CgrXmlCfgCdrField{Name: "Account", Type: CDRFIELD, Value: utils.ACCOUNT, Width: 12, Strip: "left", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Subject", Type: CDRFIELD, Value: utils.SUBJECT, Width: 5, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "CLI", Type: CDRFIELD, Value: "cli", Width: 15, Strip: "xright", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Destination", Type: CDRFIELD, Value: utils.DESTINATION, Width: 24, Strip: "xright", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "TOR", Type: CONSTANT, Value: "02", Width: 2},
|
||||
&config.CgrXmlCfgCdrField{Name: "SubtypeTOR", Type: CONSTANT, Value: "11", Width: 4, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "SetupTime", Type: CDRFIELD, Value: utils.SETUP_TIME, Width: 12, Strip: "right", Padding: "right", Layout: "020106150400"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Duration", Type: CDRFIELD, Value: utils.DURATION, Width: 6, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "DataVolume", Type: FILLER, Width: 6, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "TaxCode", Type: CONSTANT, Value: "1", Width: 1},
|
||||
&config.CgrXmlCfgCdrField{Name: "OperatorCode", Type: CDRFIELD, Value: "opercode", Width: 2, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "ProductId", Type: CDRFIELD, Value: "productid", Width: 5, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "NetworkId", Type: CONSTANT, Value: "3", Width: 1},
|
||||
&config.CgrXmlCfgCdrField{Name: "CallId", Type: CDRFIELD, Value: utils.ACCID, Width: 16, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Filler", Type: FILLER, Width: 8, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Filler", Type: FILLER, Width: 8, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "TerminationCode", Type: CONCATENATED_CDRFIELD, Value: "operator,product", Width: 5, Strip: "right", Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Cost", Type: CDRFIELD, Value: utils.COST, Width: 9, Padding: "zeroleft"},
|
||||
&config.CgrXmlCfgCdrField{Name: "DestinationPrivacy", Type: CDRFIELD, Value: "destination_privacy", Width: 1, Strip: "right", Padding: "right"},
|
||||
}
|
||||
|
||||
var trailerCfgFlds = []*config.CgrXmlCfgCdrField{
|
||||
&config.CgrXmlCfgCdrField{Name: "TypeOfRecord", Type: CONSTANT, Value: "90", Width: 2},
|
||||
&config.CgrXmlCfgCdrField{Name: "Filler1", Type: FILLER, Width: 3, Padding: "right"},
|
||||
&config.CgrXmlCfgCdrField{Name: "DistributorCode", Type: CONSTANT, Value: "VOI", Width: 3},
|
||||
&config.CgrXmlCfgCdrField{Name: "FileSeqNr", Type: METATAG, Value: META_EXPORTID, Width: 5, Strip: "right", Padding: "zeroleft"},
|
||||
&config.CgrXmlCfgCdrField{Name: "NumberOfRecords", Type: METATAG, Value: META_NRCDRS, Width: 6, Padding: "zeroleft"},
|
||||
&config.CgrXmlCfgCdrField{Name: "CdrsDuration", Type: METATAG, Value: META_DURCDRS, Width: 8, Padding: "zeroleft"},
|
||||
&config.CgrXmlCfgCdrField{Name: "FirstCdrTime", Type: METATAG, Value: META_FIRSTCDRTIME, Width: 12, Layout: "020106150400"},
|
||||
&config.CgrXmlCfgCdrField{Name: "LastCdrTime", Type: METATAG, Value: META_LASTCDRTIME, Width: 12, Layout: "020106150400"},
|
||||
&config.CgrXmlCfgCdrField{Name: "Filler2", Type: FILLER, Width: 93, Padding: "right"},
|
||||
}
|
||||
|
||||
// Write one CDR and test it's results only for content buffer
|
||||
func TestWriteCdr(t *testing.T) {
|
||||
wrBuf := &bytes.Buffer{}
|
||||
exportTpl := &config.CgrXmlCdreFwCfg{Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
|
||||
Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
|
||||
Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
|
||||
}
|
||||
fwWriter := FixedWidthCdrWriter{writer: wrBuf, exportTemplate: exportTpl, roundDecimals: 4, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}
|
||||
cdr := &utils.StoredCdr{CgrId: utils.FSCgrId("dsafdsaf"), AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
|
||||
TOR: "call", Account: "1001", Subject: "1001", Destination: "1002",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Duration: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.34567,
|
||||
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
|
||||
}
|
||||
if err := fwWriter.WriteCdr(cdr); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
eContentOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.3457 \n"
|
||||
contentOut := fwWriter.content.String()
|
||||
if len(contentOut) != 145 {
|
||||
t.Error("Unexpected content length", len(contentOut))
|
||||
} else if contentOut != eContentOut {
|
||||
t.Errorf("Content out different than expected. Have <%s>, expecting: <%s>", contentOut, eContentOut)
|
||||
}
|
||||
eHeader := "10 VOI0000007111308420024031415390001 \n"
|
||||
eTrailer := "90 VOI0000000000100000010071113084200071113084200 \n"
|
||||
outBeforeWrite := ""
|
||||
if wrBuf.String() != outBeforeWrite {
|
||||
t.Errorf("Output buffer should be empty before write")
|
||||
}
|
||||
fwWriter.Close()
|
||||
allOut := wrBuf.String()
|
||||
eAllOut := eHeader + eContentOut + eTrailer
|
||||
if math.Mod(float64(len(allOut)), 145) != 0 {
|
||||
t.Error("Unexpected export content length", len(allOut))
|
||||
} else if len(allOut) != len(eAllOut) {
|
||||
t.Errorf("Output does not match expected length. Have output %q, expecting: %q", allOut, eAllOut)
|
||||
}
|
||||
// Test stats
|
||||
if !fwWriter.firstCdrTime.Equal(cdr.SetupTime) {
|
||||
t.Error("Unexpected firstCdrTime in stats: ", fwWriter.firstCdrTime)
|
||||
} else if !fwWriter.lastCdrTime.Equal(cdr.SetupTime) {
|
||||
t.Error("Unexpected lastCdrTime in stats: ", fwWriter.lastCdrTime)
|
||||
} else if fwWriter.numberOfRecords != 1 {
|
||||
t.Error("Unexpected number of records in the stats: ", fwWriter.numberOfRecords)
|
||||
} else if fwWriter.totalDuration != cdr.Duration {
|
||||
t.Error("Unexpected total duration in the stats: ", fwWriter.totalDuration)
|
||||
} else if fwWriter.totalCost != utils.Round(cdr.Cost, fwWriter.roundDecimals, utils.ROUNDING_MIDDLE) {
|
||||
t.Error("Unexpected total cost in the stats: ", fwWriter.totalCost)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCdrs(t *testing.T) {
|
||||
wrBuf := &bytes.Buffer{}
|
||||
exportTpl := &config.CgrXmlCdreFwCfg{Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
|
||||
Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
|
||||
Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
|
||||
}
|
||||
fwWriter := FixedWidthCdrWriter{writer: wrBuf, exportTemplate: exportTpl, roundDecimals: 4, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}
|
||||
cdr1 := &utils.StoredCdr{CgrId: utils.FSCgrId("aaa1"), AccId: "aaa1", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
|
||||
TOR: "call", Account: "1001", Subject: "1001", Destination: "1010",
|
||||
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
|
||||
Duration: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.25,
|
||||
ExtraFields: map[string]string{"productnumber": "12341", "fieldextr2": "valextr2"},
|
||||
}
|
||||
cdr2 := &utils.StoredCdr{CgrId: utils.FSCgrId("aaa2"), AccId: "aaa2", CdrHost: "192.168.1.2", ReqType: "prepaid", Direction: "*out", Tenant: "cgrates.org",
|
||||
TOR: "call", Account: "1002", Subject: "1002", Destination: "1011",
|
||||
SetupTime: time.Date(2013, 11, 7, 7, 42, 20, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 7, 42, 26, 0, time.UTC),
|
||||
Duration: time.Duration(5) * time.Minute, MediationRunId: utils.DEFAULT_RUNID, Cost: 1.40001,
|
||||
ExtraFields: map[string]string{"productnumber": "12342", "fieldextr2": "valextr2"},
|
||||
}
|
||||
cdr3 := &utils.StoredCdr{}
|
||||
cdr4 := &utils.StoredCdr{CgrId: utils.FSCgrId("aaa3"), AccId: "aaa4", CdrHost: "192.168.1.4", ReqType: "postpaid", Direction: "*out", Tenant: "cgrates.org",
|
||||
TOR: "call", Account: "1004", Subject: "1004", Destination: "1013",
|
||||
SetupTime: time.Date(2013, 11, 7, 9, 42, 18, 0, time.UTC),
|
||||
AnswerTime: time.Date(2013, 11, 7, 9, 42, 26, 0, time.UTC),
|
||||
Duration: time.Duration(20) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.34567,
|
||||
ExtraFields: map[string]string{"productnumber": "12344", "fieldextr2": "valextr2"},
|
||||
}
|
||||
for _, cdr := range []*utils.StoredCdr{cdr1, cdr2, cdr3, cdr4} {
|
||||
if err := fwWriter.WriteCdr(cdr); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
contentOut := fwWriter.content.String()
|
||||
if math.Mod(float64(len(contentOut)), 145) != 0 { // Rest must be 0 always, so content is always multiple of 145 which is our row fixLength
|
||||
t.Error("Unexpected content length", len(contentOut))
|
||||
}
|
||||
}
|
||||
if len(wrBuf.String()) != 0 {
|
||||
t.Errorf("Output buffer should be empty before write")
|
||||
}
|
||||
fwWriter.Close()
|
||||
if len(wrBuf.String()) != 725 {
|
||||
t.Error("Output buffer does not contain expected info. Expecting len: 725, got: ", len(wrBuf.String()))
|
||||
}
|
||||
// Test stats
|
||||
if !fwWriter.firstCdrTime.Equal(cdr2.SetupTime) {
|
||||
t.Error("Unexpected firstCdrTime in stats: ", fwWriter.firstCdrTime)
|
||||
}
|
||||
if !fwWriter.lastCdrTime.Equal(cdr4.SetupTime) {
|
||||
t.Error("Unexpected lastCdrTime in stats: ", fwWriter.lastCdrTime)
|
||||
}
|
||||
if fwWriter.numberOfRecords != 3 {
|
||||
t.Error("Unexpected number of records in the stats: ", fwWriter.numberOfRecords)
|
||||
}
|
||||
if fwWriter.totalDuration != time.Duration(330)*time.Second {
|
||||
t.Error("Unexpected total duration in the stats: ", fwWriter.totalDuration)
|
||||
}
|
||||
if fwWriter.totalCost != 5.9957 {
|
||||
t.Error("Unexpected total cost in the stats: ", fwWriter.totalCost)
|
||||
}
|
||||
}
|
||||
73
cdre/libfixedwidth.go
Normal file
73
cdre/libfixedwidth.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Used as generic function logic for various fields
|
||||
|
||||
// Attributes
|
||||
// source - the base source
|
||||
// width - the field width
|
||||
// strip - if present it will specify the strip strategy, when missing strip will not be allowed
|
||||
// padding - if present it will specify the padding strategy to use, left, right, zeroleft, zeroright
|
||||
func FmtFieldWidth(source string, width int, strip, padding string, mandatory bool) (string, error) {
|
||||
if mandatory && len(source) == 0 {
|
||||
return "", errors.New("Empty source value")
|
||||
}
|
||||
if len(source) == width { // the source is exactly the maximum length
|
||||
return source, nil
|
||||
}
|
||||
if len(source) > width { //the source is bigger than allowed
|
||||
if len(strip) == 0 {
|
||||
return "", fmt.Errorf("Source %s is bigger than the width %d, no strip defied", source, width)
|
||||
}
|
||||
if strip == "right" {
|
||||
return source[:width], nil
|
||||
} else if strip == "xright" {
|
||||
return source[:width-1] + "x", nil // Suffix with x to mark prefix
|
||||
} else if strip == "left" {
|
||||
diffIndx := len(source) - width
|
||||
return source[diffIndx:], nil
|
||||
} else if strip == "xleft" { // Prefix one x to mark stripping
|
||||
diffIndx := len(source) - width
|
||||
return "x" + source[diffIndx+1:], nil
|
||||
}
|
||||
} else { //the source is smaller as the maximum allowed
|
||||
if len(padding) == 0 {
|
||||
return "", fmt.Errorf("Source %s is smaller than the width %d, no padding defined", source, width)
|
||||
}
|
||||
var paddingFmt string
|
||||
switch padding {
|
||||
case "right":
|
||||
paddingFmt = fmt.Sprintf("%%-%ds", width)
|
||||
case "left":
|
||||
paddingFmt = fmt.Sprintf("%%%ds", width)
|
||||
case "zeroleft":
|
||||
paddingFmt = fmt.Sprintf("%%0%ds", width)
|
||||
}
|
||||
if len(paddingFmt) != 0 {
|
||||
return fmt.Sprintf(paddingFmt, source), nil
|
||||
}
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
@@ -16,14 +16,21 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdrexporter
|
||||
package cdre
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMandatory(t *testing.T) {
|
||||
_, err := FmtFieldWidth("", 0, "", "", true)
|
||||
if err == nil {
|
||||
t.Errorf("Failed to detect mandatory value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxLen(t *testing.T) {
|
||||
result, err := filterField("test", 4, false, false, false, false)
|
||||
result, err := FmtFieldWidth("test", 4, "", "", false)
|
||||
expected := "test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"test\" was \"%s\"", result)
|
||||
@@ -31,56 +38,79 @@ func TestMaxLen(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRPadding(t *testing.T) {
|
||||
result, err := filterField("test", 8, false, false, false, false)
|
||||
result, err := FmtFieldWidth("test", 8, "", "right", false)
|
||||
expected := "test "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingFiller(t *testing.T) {
|
||||
result, err := FmtFieldWidth("", 8, "", "right", false)
|
||||
expected := " "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLPadding(t *testing.T) {
|
||||
result, err := filterField("test", 8, false, false, true, false)
|
||||
result, err := FmtFieldWidth("test", 8, "", "left", false)
|
||||
expected := " test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroLPadding(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 8, "", "zeroleft", false)
|
||||
expected := "0000test"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRStrip(t *testing.T) {
|
||||
result, err := filterField("test", 2, true, false, false, false)
|
||||
result, err := FmtFieldWidth("test", 2, "right", "", false)
|
||||
expected := "te"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXRStrip(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 3, "xright", "", false)
|
||||
expected := "tex"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLStrip(t *testing.T) {
|
||||
result, err := filterField("test", 2, true, true, false, false)
|
||||
result, err := FmtFieldWidth("test", 2, "left", "", false)
|
||||
expected := "st"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXLStrip(t *testing.T) {
|
||||
result, err := FmtFieldWidth("test", 3, "xleft", "", false)
|
||||
expected := "xst"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripNotAllowed(t *testing.T) {
|
||||
_, err := filterField("test", 2, false, false, false, false)
|
||||
_, err := FmtFieldWidth("test", 3, "", "", false)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLZeroPadding(t *testing.T) {
|
||||
result, err := filterField("12", 8, false, false, true, true)
|
||||
expected := "00000012"
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRZeroPadding(t *testing.T) {
|
||||
result, err := filterField("12", 8, false, false, false, true)
|
||||
expected := "12 "
|
||||
if err != nil || result != expected {
|
||||
t.Errorf("Expected \"%s \" was \"%s\"", expected, result)
|
||||
func TestPaddingNotAllowed(t *testing.T) {
|
||||
_, err := FmtFieldWidth("test", 5, "", "", false)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdrexporter
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type CsvCdrWriter struct {
|
||||
writer *csv.Writer
|
||||
roundDecimals int // Round floats like Cost using this number of decimals
|
||||
extraFields []string // Extra fields to append after primary ones, order important
|
||||
}
|
||||
|
||||
func NewCsvCdrWriter(writer io.Writer, roundDecimals int, extraFields []string) *CsvCdrWriter {
|
||||
return &CsvCdrWriter{csv.NewWriter(writer), roundDecimals, extraFields}
|
||||
}
|
||||
|
||||
func (dcw *CsvCdrWriter) Write(cdr *utils.RatedCDR) error {
|
||||
primaryFields := []string{cdr.CgrId, cdr.MediationRunId, cdr.AccId, cdr.CdrHost, cdr.ReqType, cdr.Direction, cdr.Tenant, cdr.TOR, cdr.Account, cdr.Subject,
|
||||
cdr.Destination, cdr.AnswerTime.String(), strconv.Itoa(int(cdr.Duration)), strconv.FormatFloat(cdr.Cost, 'f', dcw.roundDecimals, 64)}
|
||||
if len(dcw.extraFields) == 0 {
|
||||
dcw.extraFields = utils.MapKeys(cdr.ExtraFields)
|
||||
sort.Strings(dcw.extraFields) // Controlled order in case of dynamic extra fields
|
||||
}
|
||||
lenPrimary := len(primaryFields)
|
||||
row := make([]string, lenPrimary+len(dcw.extraFields))
|
||||
for idx, fld := range primaryFields { // Add primary fields
|
||||
row[idx] = fld
|
||||
}
|
||||
for idx, fldKey := range dcw.extraFields { // Add extra fields
|
||||
row[lenPrimary+idx] = cdr.ExtraFields[fldKey]
|
||||
}
|
||||
return dcw.writer.Write(row)
|
||||
}
|
||||
|
||||
func (dcw *CsvCdrWriter) Close() {
|
||||
dcw.writer.Flush()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package cdrexporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type DeanCdrWriter struct{}
|
||||
|
||||
func (dcw *DeanCdrWriter) Write(cdr []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Used as generic function logic for various fields
|
||||
|
||||
// Attributes
|
||||
// source - the base source
|
||||
// maxLen - the maximum field lenght
|
||||
// stripAllowed - whether we allow stripping of chars in case of source bigger than the maximum allowed
|
||||
// lStrip - if true, strip from beginning of the string
|
||||
// lPadding - if true, add chars at the beginning of the string
|
||||
// paddingChar - the character wich will be used to fill the existing
|
||||
func filterField(source string, maxLen int, stripAllowed, lStrip, lPadding, padWithZero bool) (string, error) {
|
||||
if len(source) == maxLen { // the source is exactly the maximum length
|
||||
return source, nil
|
||||
}
|
||||
if len(source) > maxLen { //the source is bigger than allowed
|
||||
if !stripAllowed {
|
||||
return "", fmt.Errorf("source %s is bigger than the maximum allowed length %s", source, maxLen)
|
||||
}
|
||||
if !lStrip {
|
||||
return source[:maxLen], nil
|
||||
} else {
|
||||
diffIndx := len(source) - maxLen
|
||||
return source[diffIndx:], nil
|
||||
}
|
||||
} else { //the source is smaller as the maximum allowed
|
||||
paddingString := "%"
|
||||
if padWithZero {
|
||||
paddingString += "0" // it will not work for rPadding but this is not needed
|
||||
}
|
||||
if !lPadding {
|
||||
paddingString += "-"
|
||||
}
|
||||
paddingString += strconv.Itoa(maxLen) + "s"
|
||||
return fmt.Sprintf(paddingString, source), nil
|
||||
}
|
||||
}
|
||||
67
cdrs/cdrs.go
67
cdrs/cdrs.go
@@ -20,12 +20,13 @@ package cdrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/mediator"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,36 +35,42 @@ var (
|
||||
medi *mediator.Mediator
|
||||
)
|
||||
|
||||
func fsCdrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
if fsCdr, err := new(FSCdr).New(body); err == nil {
|
||||
storage.SetCdr(fsCdr)
|
||||
go func() { //FS will not send us hangup_complete until we have send back the answer to CDR, so we need to handle mediation async
|
||||
if cfg.CDRSMediator == "internal" {
|
||||
medi.MediateRawCDR(fsCdr)
|
||||
} else {
|
||||
//TODO: use the connection to mediator
|
||||
// Returns error if not able to properly store the CDR, mediation is async since we can always recover offline
|
||||
func storeAndMediate(rawCdr utils.RawCDR) error {
|
||||
if err := storage.SetCdr(rawCdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.CDRSMediator == utils.INTERNAL {
|
||||
go func() {
|
||||
if err := medi.RateCdr(rawCdr); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("Could not run mediation on CDR: %s", err.Error()))
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler for generic cgr cdr http
|
||||
func cgrCdrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cgrCdr, err := utils.NewCgrCdrFromHttpReq(r)
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("Could not create CDR entry: %s", err.Error()))
|
||||
}
|
||||
if err := storeAndMediate(cgrCdr); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("Errors when storing CDR entry: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func cgrCdrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if cgrCdr, err := utils.NewCgrCdrFromHttpReq(r); err == nil {
|
||||
storage.SetCdr(cgrCdr)
|
||||
if cfg.CDRSMediator == "internal" {
|
||||
errMedi := medi.MediateRawCDR(cgrCdr)
|
||||
if errMedi != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("Could not run mediation on CDR: %s", errMedi.Error()))
|
||||
}
|
||||
} else {
|
||||
//TODO: use the connection to mediator
|
||||
}
|
||||
} else {
|
||||
// Handler for fs http
|
||||
func fsCdrHandler(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
fsCdr, err := new(FSCdr).New(body)
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("Could not create CDR entry: %s", err.Error()))
|
||||
}
|
||||
if err := storeAndMediate(fsCdr); err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("Errors when storing CDR entry: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
type CDRS struct{}
|
||||
@@ -75,8 +82,12 @@ func New(s engine.CdrStorage, m *mediator.Mediator, c *config.CGRConfig) *CDRS {
|
||||
return &CDRS{}
|
||||
}
|
||||
|
||||
func (cdrs *CDRS) StartCapturingCDRs() {
|
||||
http.HandleFunc("/cgr", cgrCdrHandler) // Attach CGR CDR Handler
|
||||
http.HandleFunc("/freeswitch_json", fsCdrHandler) // Attach FreeSWITCH JSON CDR Handler
|
||||
http.ListenAndServe(cfg.CDRSListen, nil)
|
||||
func (cdrs *CDRS) RegisterHanlersToServer(server *engine.Server) {
|
||||
server.RegisterHttpFunc("/cgr", cgrCdrHandler)
|
||||
server.RegisterHttpFunc("/freeswitch_json", fsCdrHandler)
|
||||
}
|
||||
|
||||
// Used to internally process CDR
|
||||
func (cdrs *CDRS) ProcessRawCdr(rawCdr utils.RawCDR) error {
|
||||
return storeAndMediate(rawCdr)
|
||||
}
|
||||
|
||||
130
cdrs/fscdr.go
130
cdrs/fscdr.go
@@ -22,10 +22,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,25 +44,29 @@ const (
|
||||
FS_CSTMID = "cgr_tenant"
|
||||
FS_CALL_DEST_NR = "dialed_extension"
|
||||
FS_PARK_TIME = "start_epoch"
|
||||
FS_SETUP_TIME = "start_epoch"
|
||||
FS_ANSWER_TIME = "answer_epoch"
|
||||
FS_HANGUP_TIME = "end_epoch"
|
||||
FS_DURATION = "billsec"
|
||||
FS_USERNAME = "user_name"
|
||||
FS_IP = "sip_local_network_addr"
|
||||
FS_CDR_SOURCE = "freeswitch_json"
|
||||
FS_SIP_REQUSER = "sip_req_user" // Apps like FusionPBX do not set dialed_extension, alternative being destination_number but that comes in customer profile, not in vars
|
||||
)
|
||||
|
||||
type FSCdr map[string]string
|
||||
type FSCdr struct {
|
||||
vars map[string]string
|
||||
body map[string]interface{} // keeps the loaded body for extra field search
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) New(body []byte) (utils.RawCDR, error) {
|
||||
fsCdr = make(map[string]string)
|
||||
var tmp map[string]interface{}
|
||||
fsCdr.vars = make(map[string]string)
|
||||
var err error
|
||||
if err = json.Unmarshal(body, &tmp); err == nil {
|
||||
if variables, ok := tmp[FS_CDR_MAP]; ok {
|
||||
if err = json.Unmarshal(body, &fsCdr.body); err == nil {
|
||||
if variables, ok := fsCdr.body[FS_CDR_MAP]; ok {
|
||||
if variables, ok := variables.(map[string]interface{}); ok {
|
||||
for k, v := range variables {
|
||||
fsCdr[k] = v.(string)
|
||||
fsCdr.vars[k] = v.(string)
|
||||
}
|
||||
}
|
||||
return fsCdr, nil
|
||||
@@ -69,13 +76,13 @@ func (fsCdr FSCdr) New(body []byte) (utils.RawCDR, error) {
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) GetCgrId() string {
|
||||
return utils.FSCgrId(fsCdr[FS_UUID])
|
||||
return utils.FSCgrId(fsCdr.vars[FS_UUID])
|
||||
}
|
||||
func (fsCdr FSCdr) GetAccId() string {
|
||||
return fsCdr[FS_UUID]
|
||||
return fsCdr.vars[FS_UUID]
|
||||
}
|
||||
func (fsCdr FSCdr) GetCdrHost() string {
|
||||
return fsCdr[FS_IP]
|
||||
return fsCdr.vars[FS_IP]
|
||||
}
|
||||
func (fsCdr FSCdr) GetCdrSource() string {
|
||||
return FS_CDR_SOURCE
|
||||
@@ -85,49 +92,88 @@ func (fsCdr FSCdr) GetDirection() string {
|
||||
return "*out"
|
||||
}
|
||||
func (fsCdr FSCdr) GetSubject() string {
|
||||
return utils.FirstNonEmpty(fsCdr[FS_SUBJECT], fsCdr[FS_USERNAME])
|
||||
return utils.FirstNonEmpty(fsCdr.vars[FS_SUBJECT], fsCdr.vars[FS_USERNAME])
|
||||
}
|
||||
func (fsCdr FSCdr) GetAccount() string {
|
||||
return utils.FirstNonEmpty(fsCdr[FS_ACCOUNT], fsCdr[FS_USERNAME])
|
||||
return utils.FirstNonEmpty(fsCdr.vars[FS_ACCOUNT], fsCdr.vars[FS_USERNAME])
|
||||
}
|
||||
|
||||
// Charging destination number
|
||||
func (fsCdr FSCdr) GetDestination() string {
|
||||
return utils.FirstNonEmpty(fsCdr[FS_DESTINATION], fsCdr[FS_CALL_DEST_NR])
|
||||
return utils.FirstNonEmpty(fsCdr.vars[FS_DESTINATION], fsCdr.vars[FS_CALL_DEST_NR], fsCdr.vars[FS_SIP_REQUSER])
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) GetTOR() string {
|
||||
return utils.FirstNonEmpty(fsCdr[FS_TOR], cfg.DefaultTOR)
|
||||
return utils.FirstNonEmpty(fsCdr.vars[FS_TOR], cfg.DefaultTOR)
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) GetTenant() string {
|
||||
return utils.FirstNonEmpty(fsCdr[FS_CSTMID], cfg.DefaultTenant)
|
||||
return utils.FirstNonEmpty(fsCdr.vars[FS_CSTMID], cfg.DefaultTenant)
|
||||
}
|
||||
func (fsCdr FSCdr) GetReqType() string {
|
||||
return utils.FirstNonEmpty(fsCdr[FS_REQTYPE], cfg.DefaultReqType)
|
||||
return utils.FirstNonEmpty(fsCdr.vars[FS_REQTYPE], cfg.DefaultReqType)
|
||||
}
|
||||
func (fsCdr FSCdr) GetExtraFields() map[string]string {
|
||||
extraFields := make(map[string]string, len(cfg.CDRSExtraFields))
|
||||
for _, field := range cfg.CDRSExtraFields {
|
||||
extraFields[field] = fsCdr[field]
|
||||
origFieldVal, foundInVars := fsCdr.vars[field.Id]
|
||||
if !foundInVars {
|
||||
origFieldVal = fsCdr.searchExtraField(field.Id, fsCdr.body)
|
||||
}
|
||||
extraFields[field.Id] = field.ParseValue(origFieldVal)
|
||||
}
|
||||
return extraFields
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) searchExtraField(field string, body map[string]interface{}) (result string) {
|
||||
for key, value := range body {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if key == field {
|
||||
return v
|
||||
}
|
||||
case map[string]interface{}:
|
||||
if result = fsCdr.searchExtraField(field, v); result != "" {
|
||||
return
|
||||
}
|
||||
case []interface{}:
|
||||
for _, item := range v {
|
||||
if otherMap, ok := item.(map[string]interface{}); ok {
|
||||
if result = fsCdr.searchExtraField(field, otherMap); result != "" {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
engine.Logger.Warning(fmt.Sprintf("Slice with no maps: %v", reflect.TypeOf(item)))
|
||||
}
|
||||
}
|
||||
default:
|
||||
engine.Logger.Warning(fmt.Sprintf("Unexpected type: %v", reflect.TypeOf(v)))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (fsCdr FSCdr) GetSetupTime() (t time.Time, err error) {
|
||||
//ToDo: Make sure we work with UTC instead of local time
|
||||
at, err := strconv.ParseInt(fsCdr.vars[FS_SETUP_TIME], 0, 64)
|
||||
t = time.Unix(at, 0)
|
||||
return
|
||||
}
|
||||
func (fsCdr FSCdr) GetAnswerTime() (t time.Time, err error) {
|
||||
//ToDo: Make sure we work with UTC instead of local time
|
||||
at, err := strconv.ParseInt(fsCdr[FS_ANSWER_TIME], 0, 64)
|
||||
at, err := strconv.ParseInt(fsCdr.vars[FS_ANSWER_TIME], 0, 64)
|
||||
t = time.Unix(at, 0)
|
||||
return
|
||||
}
|
||||
func (fsCdr FSCdr) GetHangupTime() (t time.Time, err error) {
|
||||
hupt, err := strconv.ParseInt(fsCdr[FS_HANGUP_TIME], 0, 64)
|
||||
hupt, err := strconv.ParseInt(fsCdr.vars[FS_HANGUP_TIME], 0, 64)
|
||||
t = time.Unix(hupt, 0)
|
||||
return
|
||||
}
|
||||
|
||||
// Extracts duration as considered by the telecom switch
|
||||
func (fsCdr FSCdr) GetDuration() time.Duration {
|
||||
dur, _ := utils.ParseDurationWithSecs(fsCdr[FS_DURATION])
|
||||
dur, _ := utils.ParseDurationWithSecs(fsCdr.vars[FS_DURATION])
|
||||
return dur
|
||||
}
|
||||
|
||||
@@ -162,17 +208,17 @@ func (fsCdr FSCdr) Restore(input string) error {
|
||||
}
|
||||
|
||||
// Used in extra mediation
|
||||
func (fsCdr FSCdr) AsRatedCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, answerTimeFld, durationFld string, extraFlds []string, fieldsMandatory bool) (*utils.RatedCDR, error) {
|
||||
func (fsCdr FSCdr) AsStoredCdr(runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, setupTimeFld, answerTimeFld, durationFld string, extraFlds []string, fieldsMandatory bool) (*utils.StoredCdr, error) {
|
||||
if utils.IsSliceMember([]string{runId, reqTypeFld, directionFld, tenantFld, torFld, accountFld, subjectFld, destFld, answerTimeFld, durationFld}, "") {
|
||||
return nil, errors.New(fmt.Sprintf("%s:FieldName", utils.ERR_MANDATORY_IE_MISSING)) // All input field names are mandatory
|
||||
}
|
||||
var err error
|
||||
var hasKey bool
|
||||
var aTimeStr, durStr string
|
||||
rtCdr := new(utils.RatedCDR)
|
||||
var sTimeStr, aTimeStr, durStr string
|
||||
rtCdr := new(utils.StoredCdr)
|
||||
rtCdr.MediationRunId = runId
|
||||
rtCdr.Cost = -1.0 // Default for non-rated CDR
|
||||
if rtCdr.AccId = fsCdr.GetAccId(); len(rtCdr.AccId)==0 {
|
||||
if rtCdr.AccId = fsCdr.GetAccId(); len(rtCdr.AccId) == 0 {
|
||||
if fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, utils.ACCID))
|
||||
} else { // Not mandatory, need to generate here CgrId
|
||||
@@ -181,48 +227,58 @@ func (fsCdr FSCdr) AsRatedCdr(runId, reqTypeFld, directionFld, tenantFld, torFld
|
||||
} else { // hasKey, use it to generate cgrid
|
||||
rtCdr.CgrId = utils.FSCgrId(rtCdr.AccId)
|
||||
}
|
||||
if rtCdr.CdrHost = fsCdr.GetCdrHost(); len(rtCdr.CdrHost)==0 && fieldsMandatory {
|
||||
if rtCdr.CdrHost = fsCdr.GetCdrHost(); len(rtCdr.CdrHost) == 0 && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, utils.CDRHOST))
|
||||
}
|
||||
if rtCdr.CdrSource = fsCdr.GetCdrSource(); len(rtCdr.CdrSource)==0 && fieldsMandatory {
|
||||
if rtCdr.CdrSource = fsCdr.GetCdrSource(); len(rtCdr.CdrSource) == 0 && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, utils.CDRSOURCE))
|
||||
}
|
||||
if strings.HasPrefix(reqTypeFld, utils.STATIC_VALUE_PREFIX) { // Values starting with prefix are not dynamically populated
|
||||
rtCdr.ReqType = reqTypeFld[1:]
|
||||
} else if rtCdr.ReqType, hasKey = fsCdr[reqTypeFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.ReqType, hasKey = fsCdr.vars[reqTypeFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, reqTypeFld))
|
||||
}
|
||||
if strings.HasPrefix(directionFld, utils.STATIC_VALUE_PREFIX) {
|
||||
rtCdr.Direction = directionFld[1:]
|
||||
} else if rtCdr.Direction, hasKey = fsCdr[directionFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.Direction, hasKey = fsCdr.vars[directionFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, directionFld))
|
||||
}
|
||||
if strings.HasPrefix(tenantFld, utils.STATIC_VALUE_PREFIX) {
|
||||
rtCdr.Tenant = tenantFld[1:]
|
||||
} else if rtCdr.Tenant, hasKey = fsCdr[tenantFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.Tenant, hasKey = fsCdr.vars[tenantFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, tenantFld))
|
||||
}
|
||||
if strings.HasPrefix(torFld, utils.STATIC_VALUE_PREFIX) {
|
||||
rtCdr.TOR = torFld[1:]
|
||||
} else if rtCdr.TOR, hasKey = fsCdr[torFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.TOR, hasKey = fsCdr.vars[torFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, torFld))
|
||||
}
|
||||
if strings.HasPrefix(accountFld, utils.STATIC_VALUE_PREFIX) {
|
||||
rtCdr.Account = accountFld[1:]
|
||||
} else if rtCdr.Account, hasKey = fsCdr[accountFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.Account, hasKey = fsCdr.vars[accountFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, accountFld))
|
||||
}
|
||||
if strings.HasPrefix(subjectFld, utils.STATIC_VALUE_PREFIX) {
|
||||
rtCdr.Subject = subjectFld[1:]
|
||||
} else if rtCdr.Subject, hasKey = fsCdr[subjectFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.Subject, hasKey = fsCdr.vars[subjectFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, subjectFld))
|
||||
}
|
||||
if strings.HasPrefix(destFld, utils.STATIC_VALUE_PREFIX) {
|
||||
rtCdr.Destination = destFld[1:]
|
||||
} else if rtCdr.Destination, hasKey = fsCdr[destFld]; !hasKey && fieldsMandatory {
|
||||
} else if rtCdr.Destination, hasKey = fsCdr.vars[destFld]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, destFld))
|
||||
}
|
||||
if aTimeStr, hasKey = fsCdr[answerTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(answerTimeFld, utils.STATIC_VALUE_PREFIX) {
|
||||
if sTimeStr, hasKey = fsCdr.vars[setupTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(setupTimeFld, utils.STATIC_VALUE_PREFIX) {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, setupTimeFld))
|
||||
} else {
|
||||
if strings.HasPrefix(setupTimeFld, utils.STATIC_VALUE_PREFIX) {
|
||||
sTimeStr = setupTimeFld[1:]
|
||||
}
|
||||
if rtCdr.SetupTime, err = utils.ParseTimeDetectLayout(sTimeStr); err != nil && fieldsMandatory {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if aTimeStr, hasKey = fsCdr.vars[answerTimeFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(answerTimeFld, utils.STATIC_VALUE_PREFIX) {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, answerTimeFld))
|
||||
} else {
|
||||
if strings.HasPrefix(answerTimeFld, utils.STATIC_VALUE_PREFIX) {
|
||||
@@ -232,7 +288,7 @@ func (fsCdr FSCdr) AsRatedCdr(runId, reqTypeFld, directionFld, tenantFld, torFld
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if durStr, hasKey = fsCdr[durationFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(durationFld, utils.STATIC_VALUE_PREFIX){
|
||||
if durStr, hasKey = fsCdr.vars[durationFld]; !hasKey && fieldsMandatory && !strings.HasPrefix(durationFld, utils.STATIC_VALUE_PREFIX) {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, durationFld))
|
||||
} else {
|
||||
if strings.HasPrefix(durationFld, utils.STATIC_VALUE_PREFIX) {
|
||||
@@ -244,7 +300,7 @@ func (fsCdr FSCdr) AsRatedCdr(runId, reqTypeFld, directionFld, tenantFld, torFld
|
||||
}
|
||||
rtCdr.ExtraFields = make(map[string]string, len(extraFlds))
|
||||
for _, fldName := range extraFlds {
|
||||
if fldVal, hasKey := fsCdr[fldName]; !hasKey && fieldsMandatory {
|
||||
if fldVal, hasKey := fsCdr.vars[fldName]; !hasKey && fieldsMandatory {
|
||||
return nil, errors.New(fmt.Sprintf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, fldName))
|
||||
} else {
|
||||
rtCdr.ExtraFields[fldName] = fldVal
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,17 +22,14 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/apier/v1"
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
"github.com/cgrates/cgrates/balancer2go"
|
||||
"github.com/cgrates/cgrates/cdrc"
|
||||
"github.com/cgrates/cgrates/cdrs"
|
||||
@@ -46,8 +43,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DISABLED = "disabled"
|
||||
INTERNAL = "internal"
|
||||
JSON = "json"
|
||||
GOB = "gob"
|
||||
POSTGRES = "postgres"
|
||||
@@ -59,79 +54,59 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
cfgPath = flag.String("config", "/etc/cgrates/cgrates.cfg", "Configuration file location.")
|
||||
version = flag.Bool("version", false, "Prints the application version.")
|
||||
raterEnabled = flag.Bool("rater", false, "Enforce starting of the rater daemon overwriting config")
|
||||
schedEnabled = flag.Bool("scheduler", false, "Enforce starting of the scheduler daemon overwriting config")
|
||||
cdrsEnabled = flag.Bool("cdrs", false, "Enforce starting of the cdrs daemon overwriting config")
|
||||
cdrcEnabled = flag.Bool("cdrc", false, "Enforce starting of the cdrc service overwriting config")
|
||||
mediatorEnabled = flag.Bool("mediator", false, "Enforce starting of the mediator service overwriting config")
|
||||
pidFile = flag.String("pid", "", "Write pid file")
|
||||
bal = balancer2go.NewBalancer()
|
||||
exitChan = make(chan bool)
|
||||
sm sessionmanager.SessionManager
|
||||
medi *mediator.Mediator
|
||||
cfg *config.CGRConfig
|
||||
err error
|
||||
cfgPath = flag.String("config", "/etc/cgrates/cgrates.cfg", "Configuration file location.")
|
||||
version = flag.Bool("version", false, "Prints the application version.")
|
||||
raterEnabled = flag.Bool("rater", false, "Enforce starting of the rater daemon overwriting config")
|
||||
schedEnabled = flag.Bool("scheduler", false, "Enforce starting of the scheduler daemon .overwriting config")
|
||||
cdrsEnabled = flag.Bool("cdrs", false, "Enforce starting of the cdrs daemon overwriting config")
|
||||
cdrcEnabled = flag.Bool("cdrc", false, "Enforce starting of the cdrc service overwriting config")
|
||||
mediatorEnabled = flag.Bool("mediator", false, "Enforce starting of the mediator service overwriting config")
|
||||
pidFile = flag.String("pid", "", "Write pid file")
|
||||
bal = balancer2go.NewBalancer()
|
||||
exitChan = make(chan bool)
|
||||
server = &engine.Server{}
|
||||
scribeServer history.Scribe
|
||||
cdrServer *cdrs.CDRS
|
||||
sm sessionmanager.SessionManager
|
||||
medi *mediator.Mediator
|
||||
cfg *config.CGRConfig
|
||||
err error
|
||||
)
|
||||
|
||||
func listenToRPCRequests(rpcResponder interface{}, apier *apier.ApierV1, rpcAddress string, rpc_encoding string) {
|
||||
l, err := net.Listen("tcp", rpcAddress)
|
||||
if err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("<Rater> Could not listen to %v: %v", rpcAddress, err))
|
||||
func cacheData(ratingDb engine.RatingStorage, accountDb engine.AccountingStorage, doneChan chan struct{}) {
|
||||
if err := ratingDb.CacheRating(nil, nil, nil, nil); err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Cache rating error: %s", err.Error()))
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
engine.Logger.Info(fmt.Sprintf("<Rater> Listening for incomming RPC requests on %v", l.Addr()))
|
||||
rpc.Register(rpcResponder)
|
||||
rpc.Register(apier)
|
||||
var serveFunc func(io.ReadWriteCloser)
|
||||
if rpc_encoding == JSON {
|
||||
serveFunc = jsonrpc.ServeConn
|
||||
} else {
|
||||
serveFunc = rpc.ServeConn
|
||||
}
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<Rater> Accept error: %v", conn))
|
||||
continue
|
||||
}
|
||||
|
||||
engine.Logger.Info(fmt.Sprintf("<Rater> New incoming connection: %v", conn.RemoteAddr()))
|
||||
go serveFunc(conn)
|
||||
if err := accountDb.CacheAccounting(nil, nil, nil); err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Cache accounting error: %s", err.Error()))
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
close(doneChan)
|
||||
}
|
||||
|
||||
func startMediator(responder *engine.Responder, loggerDb engine.LogStorage, cdrDb engine.CdrStorage) {
|
||||
func startMediator(responder *engine.Responder, loggerDb engine.LogStorage, cdrDb engine.CdrStorage, cacheChan, chanDone chan struct{}) {
|
||||
var connector engine.Connector
|
||||
if cfg.MediatorRater == INTERNAL {
|
||||
if cfg.MediatorRater == utils.INTERNAL {
|
||||
<-cacheChan // Cache needs to come up before we are ready
|
||||
connector = responder
|
||||
} else {
|
||||
var client *rpc.Client
|
||||
var err error
|
||||
if cfg.RPCEncoding == JSON {
|
||||
for i := 0; i < cfg.MediatorRaterReconnects; i++ {
|
||||
client, err = jsonrpc.Dial("tcp", cfg.MediatorRater)
|
||||
if err == nil { //Connected so no need to reiterate
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i/2) * time.Second)
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < cfg.MediatorRaterReconnects; i++ {
|
||||
client, err = rpc.Dial("tcp", cfg.MediatorRater)
|
||||
if err == nil { //Connected so no need to reiterate
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i/2) * time.Second)
|
||||
|
||||
for i := 0; i < cfg.MediatorRaterReconnects; i++ {
|
||||
client, err = rpc.Dial("tcp", cfg.MediatorRater)
|
||||
if err == nil { //Connected so no need to reiterate
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i+1) * time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not connect to engine: %v", err))
|
||||
engine.Logger.Crit(fmt.Sprintf("<Mediator> Could not connect to engine: %v", err))
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
connector = &engine.RPCClientConnector{Client: client}
|
||||
}
|
||||
@@ -140,11 +115,19 @@ func startMediator(responder *engine.Responder, loggerDb engine.LogStorage, cdrD
|
||||
if err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Mediator config parsing error: %v", err))
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
engine.Logger.Info("Registering Mediator RPC service.")
|
||||
server.RpcRegister(&mediator.MediatorV1{Medi: medi})
|
||||
|
||||
close(chanDone)
|
||||
}
|
||||
|
||||
func startCdrc() {
|
||||
cdrc, err := cdrc.NewCdrc(cfg)
|
||||
func startCdrc(cdrsChan chan struct{}) {
|
||||
if cfg.CdrcCdrs == utils.INTERNAL {
|
||||
<-cdrsChan // Wait for CDRServer to come up before start processing
|
||||
}
|
||||
cdrc, err := cdrc.NewCdrc(cfg, cdrServer)
|
||||
if err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Cdrc config parsing error: %s", err.Error()))
|
||||
exitChan <- true
|
||||
@@ -156,34 +139,24 @@ func startCdrc() {
|
||||
exitChan <- true // If run stopped, something is bad, stop the application
|
||||
}
|
||||
|
||||
func startSessionManager(responder *engine.Responder, loggerDb engine.LogStorage) {
|
||||
func startSessionManager(responder *engine.Responder, loggerDb engine.LogStorage, cacheChan chan struct{}) {
|
||||
var connector engine.Connector
|
||||
if cfg.SMRater == INTERNAL {
|
||||
if cfg.SMRater == utils.INTERNAL {
|
||||
<-cacheChan // Wait for the cache to init before start doing queries
|
||||
connector = responder
|
||||
} else {
|
||||
var client *rpc.Client
|
||||
var err error
|
||||
if cfg.RPCEncoding == JSON {
|
||||
// We attempt to reconnect more times
|
||||
for i := 0; i < cfg.SMRaterReconnects; i++ {
|
||||
client, err = jsonrpc.Dial("tcp", cfg.SMRater)
|
||||
if err == nil { //Connected so no need to reiterate
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i/2) * time.Second)
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < cfg.SMRaterReconnects; i++ {
|
||||
client, err = rpc.Dial("tcp", cfg.SMRater)
|
||||
if err == nil { //Connected so no need to reiterate
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i/2) * time.Second)
|
||||
}
|
||||
|
||||
for i := 0; i < cfg.SMRaterReconnects; i++ {
|
||||
client, err = rpc.Dial("tcp", cfg.SMRater)
|
||||
if err == nil { //Connected so no need to reiterate
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(i+1) * time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not connect to engine: %v", err))
|
||||
engine.Logger.Crit(fmt.Sprintf("<SessionManager> Could not connect to engine: %v", err))
|
||||
exitChan <- true
|
||||
}
|
||||
connector = &engine.RPCClientConnector{Client: client}
|
||||
@@ -198,110 +171,95 @@ func startSessionManager(responder *engine.Responder, loggerDb engine.LogStorage
|
||||
}
|
||||
default:
|
||||
engine.Logger.Err(fmt.Sprintf("<SessionManager> Unsupported session manger type: %s!", cfg.SMSwitchType))
|
||||
exitChan <- true
|
||||
}
|
||||
exitChan <- true
|
||||
}
|
||||
|
||||
func startCDRS(responder *engine.Responder, cdrDb engine.CdrStorage) {
|
||||
if cfg.CDRSMediator == INTERNAL {
|
||||
for i := 0; i < 3; i++ { // ToDo: If the right approach, make the reconnects configurable
|
||||
time.Sleep(time.Duration(i/2) * time.Second)
|
||||
if medi != nil { // Got our mediator, no need to wait any longer
|
||||
break
|
||||
}
|
||||
}
|
||||
func startCDRS(responder *engine.Responder, cdrDb engine.CdrStorage, mediChan, doneChan chan struct{}) {
|
||||
if cfg.CDRSMediator == utils.INTERNAL {
|
||||
<-mediChan // Deadlock if mediator not started
|
||||
if medi == nil {
|
||||
engine.Logger.Crit("<CDRS> Could not connect to mediator, exiting.")
|
||||
exitChan <- true
|
||||
}
|
||||
}
|
||||
cs := cdrs.New(cdrDb, medi, cfg)
|
||||
cs.StartCapturingCDRs()
|
||||
exitChan <- true
|
||||
}
|
||||
|
||||
func startHistoryScribe() {
|
||||
var scribeServer history.Scribe
|
||||
|
||||
if cfg.HistoryServerEnabled {
|
||||
if scribeServer, err = history.NewFileScribe(cfg.HistoryDir, cfg.HistorySaveInterval); err != nil {
|
||||
engine.Logger.Crit(err.Error())
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
cdrServer = cdrs.New(cdrDb, medi, cfg)
|
||||
cdrServer.RegisterHanlersToServer(server)
|
||||
close(doneChan)
|
||||
}
|
||||
|
||||
if cfg.HistoryServerEnabled {
|
||||
if cfg.HistoryListen != INTERNAL {
|
||||
rpc.RegisterName("Scribe", scribeServer)
|
||||
var serveFunc func(io.ReadWriteCloser)
|
||||
if cfg.RPCEncoding == JSON {
|
||||
serveFunc = jsonrpc.ServeConn
|
||||
} else {
|
||||
serveFunc = rpc.ServeConn
|
||||
}
|
||||
l, err := net.Listen("tcp", cfg.HistoryListen)
|
||||
if err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("<History> Could not listen to %v: %v", cfg.HistoryListen, err))
|
||||
func startHistoryServer(chanDone chan struct{}) {
|
||||
if scribeServer, err = history.NewFileScribe(cfg.HistoryDir, cfg.HistorySaveInterval); err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("<HistoryServer> Could not start, error: %s", err.Error()))
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
server.RpcRegisterName("Scribe", scribeServer)
|
||||
close(chanDone)
|
||||
}
|
||||
|
||||
// chanStartServer will report when server is up, useful for internal requests
|
||||
func startHistoryAgent(scribeServer history.Scribe, chanServerStarted chan struct{}) {
|
||||
if cfg.HistoryServer == utils.INTERNAL { // For internal requests, wait for server to come online before connecting
|
||||
engine.Logger.Crit(fmt.Sprintf("<HistoryAgent> Connecting internally to HistoryServer"))
|
||||
select {
|
||||
case <-time.After(1 * time.Minute):
|
||||
engine.Logger.Crit(fmt.Sprintf("<HistoryAgent> Timeout waiting for server to start."))
|
||||
exitChan <- true
|
||||
return
|
||||
case <-chanServerStarted:
|
||||
}
|
||||
//<-chanServerStarted // If server is not enabled, will have deadlock here
|
||||
} else { // Connect in iteration since there are chances of concurrency here
|
||||
for i := 0; i < 3; i++ { //ToDo: Make it globally configurable
|
||||
//engine.Logger.Crit(fmt.Sprintf("<HistoryAgent> Trying to connect, iteration: %d, time %s", i, time.Now()))
|
||||
if scribeServer, err = history.NewProxyScribe(cfg.HistoryServer); err == nil {
|
||||
break //Connected so no need to reiterate
|
||||
} else if i == 2 && err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("<HistoryAgent> Could not connect to the server, error: %s", err.Error()))
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
engine.Logger.Err(fmt.Sprintf("<History> Accept error: %v", conn))
|
||||
continue
|
||||
}
|
||||
|
||||
engine.Logger.Info(fmt.Sprintf("<History> New incoming connection: %v", conn.RemoteAddr()))
|
||||
go serveFunc(conn)
|
||||
}
|
||||
time.Sleep(time.Duration(i) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
var scribeAgent history.Scribe
|
||||
|
||||
if cfg.HistoryAgentEnabled {
|
||||
if cfg.HistoryServer != INTERNAL { // Connect in iteration since there are chances of concurrency here
|
||||
for i := 0; i < 3; i++ { //ToDo: Make it globally configurable
|
||||
if scribeAgent, err = history.NewProxyScribe(cfg.HistoryServer, cfg.RPCEncoding); err == nil {
|
||||
break //Connected so no need to reiterate
|
||||
} else if i == 2 && err != nil {
|
||||
engine.Logger.Crit(err.Error())
|
||||
exitChan <- true
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Duration(i/2) * time.Second)
|
||||
}
|
||||
} else {
|
||||
scribeAgent = scribeServer
|
||||
}
|
||||
}
|
||||
if scribeAgent != nil {
|
||||
engine.SetHistoryScribe(scribeAgent)
|
||||
} else {
|
||||
engine.SetHistoryScribe(scribeServer) // if it is nil so be it
|
||||
}
|
||||
|
||||
engine.SetHistoryScribe(scribeServer)
|
||||
return
|
||||
}
|
||||
|
||||
// Starts the rpc server, waiting for the necessary components to finish their tasks
|
||||
func serveRpc(rpcWaitChans []chan struct{}) {
|
||||
for _, chn := range rpcWaitChans {
|
||||
<-chn
|
||||
}
|
||||
// Each of the serve blocks so need to start in their own goroutine
|
||||
go server.ServeJSON(cfg.RPCJSONListen)
|
||||
go server.ServeGOB(cfg.RPCGOBListen)
|
||||
}
|
||||
|
||||
// Starts the http server, waiting for the necessary components to finish their tasks
|
||||
func serveHttp(httpWaitChans []chan struct{}) {
|
||||
for _, chn := range httpWaitChans {
|
||||
<-chn
|
||||
}
|
||||
server.ServeHTTP(cfg.HTTPListen)
|
||||
}
|
||||
|
||||
func checkConfigSanity() error {
|
||||
if cfg.SMEnabled && cfg.RaterEnabled && cfg.RaterBalancer != DISABLED {
|
||||
if cfg.SMEnabled && cfg.RaterEnabled && cfg.RaterBalancer != "" {
|
||||
engine.Logger.Crit("The session manager must not be enabled on a worker engine (change [engine]/balancer to disabled)!")
|
||||
return errors.New("SessionManager on Worker")
|
||||
}
|
||||
if cfg.BalancerEnabled && cfg.RaterEnabled && cfg.RaterBalancer != DISABLED {
|
||||
if cfg.BalancerEnabled && cfg.RaterEnabled && cfg.RaterBalancer != "" {
|
||||
engine.Logger.Crit("The balancer is enabled so it cannot connect to another balancer (change rater/balancer to disabled)!")
|
||||
return errors.New("Improperly configured balancer")
|
||||
}
|
||||
if cfg.CDRSEnabled && cfg.CDRSMediator == INTERNAL && !cfg.MediatorEnabled {
|
||||
if cfg.CDRSEnabled && cfg.CDRSMediator == utils.INTERNAL && !cfg.MediatorEnabled {
|
||||
engine.Logger.Crit("CDRS cannot connect to mediator, Mediator not enabled in configuration!")
|
||||
return errors.New("Internal Mediator required by CDRS")
|
||||
}
|
||||
if cfg.HistoryServerEnabled && cfg.HistoryServer == INTERNAL && !cfg.HistoryServerEnabled {
|
||||
if cfg.HistoryServerEnabled && cfg.HistoryServer == utils.INTERNAL && !cfg.HistoryServerEnabled {
|
||||
engine.Logger.Crit("The history agent is enabled and internal and history server is disabled!")
|
||||
return errors.New("Improperly configured history service")
|
||||
}
|
||||
@@ -337,31 +295,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
config.SetCgrConfig(cfg) // Share the config object
|
||||
// some consitency checks
|
||||
errCfg := checkConfigSanity()
|
||||
if errCfg != nil {
|
||||
engine.Logger.Crit(errCfg.Error())
|
||||
return
|
||||
}
|
||||
var ratingDb engine.RatingStorage
|
||||
var accountDb engine.AccountingStorage
|
||||
var logDb engine.LogStorage
|
||||
var loadDb engine.LoadStorage
|
||||
var cdrDb engine.CdrStorage
|
||||
ratingDb, err = engine.ConfigureRatingStorage(cfg.RatingDBType, cfg.RatingDBHost, cfg.RatingDBPort, cfg.RatingDBName, cfg.RatingDBUser, cfg.RatingDBPass, cfg.DBDataEncoding)
|
||||
if err != nil { // Cannot configure getter database, show stopper
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not configure dataDb: %s exiting!", err))
|
||||
return
|
||||
}
|
||||
defer ratingDb.Close()
|
||||
engine.SetRatingStorage(ratingDb)
|
||||
accountDb, err = engine.ConfigureAccountingStorage(cfg.AccountDBType, cfg.AccountDBHost, cfg.AccountDBPort, cfg.AccountDBName, cfg.AccountDBUser, cfg.AccountDBPass, cfg.DBDataEncoding)
|
||||
if err != nil { // Cannot configure getter database, show stopper
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not configure dataDb: %s exiting!", err))
|
||||
return
|
||||
}
|
||||
defer accountDb.Close()
|
||||
engine.SetAccountingStorage(accountDb)
|
||||
if *raterEnabled {
|
||||
cfg.RaterEnabled = *raterEnabled
|
||||
}
|
||||
@@ -377,21 +310,40 @@ func main() {
|
||||
if *mediatorEnabled {
|
||||
cfg.MediatorEnabled = *mediatorEnabled
|
||||
}
|
||||
if cfg.RaterEnabled {
|
||||
if err := ratingDb.CacheRating(nil, nil, nil); err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Cache rating error: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
if err := accountDb.CacheAccounting(nil); err != nil {
|
||||
engine.Logger.Crit(fmt.Sprintf("Cache accounting error: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// some consitency checks
|
||||
errCfg := checkConfigSanity()
|
||||
if errCfg != nil {
|
||||
engine.Logger.Crit(errCfg.Error())
|
||||
return
|
||||
}
|
||||
var ratingDb engine.RatingStorage
|
||||
var accountDb engine.AccountingStorage
|
||||
var logDb engine.LogStorage
|
||||
var loadDb engine.LoadStorage
|
||||
var cdrDb engine.CdrStorage
|
||||
ratingDb, err = engine.ConfigureRatingStorage(cfg.RatingDBType, cfg.RatingDBHost, cfg.RatingDBPort,
|
||||
cfg.RatingDBName, cfg.RatingDBUser, cfg.RatingDBPass, cfg.DBDataEncoding)
|
||||
if err != nil { // Cannot configure getter database, show stopper
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not configure dataDb: %s exiting!", err))
|
||||
return
|
||||
}
|
||||
defer ratingDb.Close()
|
||||
engine.SetRatingStorage(ratingDb)
|
||||
accountDb, err = engine.ConfigureAccountingStorage(cfg.AccountDBType, cfg.AccountDBHost, cfg.AccountDBPort,
|
||||
cfg.AccountDBName, cfg.AccountDBUser, cfg.AccountDBPass, cfg.DBDataEncoding)
|
||||
if err != nil { // Cannot configure getter database, show stopper
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not configure dataDb: %s exiting!", err))
|
||||
return
|
||||
}
|
||||
defer accountDb.Close()
|
||||
engine.SetAccountingStorage(accountDb)
|
||||
|
||||
if cfg.StorDBType == SAME {
|
||||
logDb = ratingDb.(engine.LogStorage)
|
||||
} else {
|
||||
logDb, err = engine.ConfigureLogStorage(cfg.StorDBType, cfg.StorDBHost, cfg.StorDBPort, cfg.StorDBName, cfg.StorDBUser, cfg.StorDBPass, cfg.DBDataEncoding)
|
||||
logDb, err = engine.ConfigureLogStorage(cfg.StorDBType, cfg.StorDBHost, cfg.StorDBPort,
|
||||
cfg.StorDBName, cfg.StorDBUser, cfg.StorDBPass, cfg.DBDataEncoding)
|
||||
if err != nil { // Cannot configure logger database, show stopper
|
||||
engine.Logger.Crit(fmt.Sprintf("Could not configure logger database: %s exiting!", err))
|
||||
return
|
||||
@@ -402,36 +354,56 @@ func main() {
|
||||
// loadDb,cdrDb and logDb are all mapped on the same stordb storage
|
||||
loadDb = logDb.(engine.LoadStorage)
|
||||
cdrDb = logDb.(engine.CdrStorage)
|
||||
|
||||
engine.SetRoundingMethodAndDecimals(cfg.RoundingMethod, cfg.RoundingDecimals)
|
||||
if cfg.SMDebitInterval > 0 {
|
||||
if dp, err := time.ParseDuration(fmt.Sprintf("%vs", cfg.SMDebitInterval)); err == nil {
|
||||
engine.SetDebitPeriod(dp)
|
||||
}
|
||||
}
|
||||
|
||||
stopHandled := false
|
||||
|
||||
// Async starts here
|
||||
if cfg.RaterEnabled && cfg.RaterBalancer != DISABLED && !cfg.BalancerEnabled {
|
||||
|
||||
rpcWait := make([]chan struct{}, 0) // Rpc server will start as soon as this list is consumed
|
||||
httpWait := make([]chan struct{}, 0) // Http server will start as soon as this list is consumed
|
||||
|
||||
var cacheChan chan struct{}
|
||||
if cfg.RaterEnabled { // Cache rating if rater enabled
|
||||
cacheChan = make(chan struct{})
|
||||
rpcWait = append(rpcWait, cacheChan)
|
||||
go cacheData(ratingDb, accountDb, cacheChan)
|
||||
}
|
||||
|
||||
if cfg.RaterEnabled && cfg.RaterBalancer != "" && !cfg.BalancerEnabled {
|
||||
go registerToBalancer()
|
||||
go stopRaterSignalHandler()
|
||||
stopHandled = true
|
||||
}
|
||||
|
||||
responder := &engine.Responder{ExitChan: exitChan}
|
||||
apier := &apier.ApierV1{StorDb: loadDb, RatingDb: ratingDb, AccountDb: accountDb, CdrDb: cdrDb, Config: cfg}
|
||||
if cfg.RaterEnabled && !cfg.BalancerEnabled && cfg.RaterListen != INTERNAL {
|
||||
engine.Logger.Info(fmt.Sprintf("Starting CGRateS Rater on %s.", cfg.RaterListen))
|
||||
go listenToRPCRequests(responder, apier, cfg.RaterListen, cfg.RPCEncoding)
|
||||
apier := &apier.ApierV1{StorDb: loadDb, RatingDb: ratingDb, AccountDb: accountDb, CdrDb: cdrDb, LogDb: logDb, Config: cfg}
|
||||
|
||||
if cfg.RaterEnabled && !cfg.BalancerEnabled && cfg.RaterBalancer != utils.INTERNAL {
|
||||
engine.Logger.Info("Registering Rater service")
|
||||
server.RpcRegister(responder)
|
||||
server.RpcRegister(apier)
|
||||
}
|
||||
|
||||
if cfg.BalancerEnabled {
|
||||
engine.Logger.Info(fmt.Sprintf("Starting CGRateS Balancer on %s.", cfg.BalancerListen))
|
||||
engine.Logger.Info("Registering Balancer service.")
|
||||
go stopBalancerSignalHandler()
|
||||
stopHandled = true
|
||||
responder.Bal = bal
|
||||
go listenToRPCRequests(responder, apier, cfg.BalancerListen, cfg.RPCEncoding)
|
||||
server.RpcRegister(responder)
|
||||
server.RpcRegister(apier)
|
||||
if cfg.RaterEnabled {
|
||||
engine.Logger.Info("Starting internal engine.")
|
||||
engine.Logger.Info("<Balancer> Registering internal rater")
|
||||
bal.AddClient("local", new(engine.ResponderWorker))
|
||||
}
|
||||
}
|
||||
|
||||
if !stopHandled {
|
||||
go generalSignalHandler()
|
||||
}
|
||||
@@ -447,32 +419,51 @@ func main() {
|
||||
}()
|
||||
}
|
||||
|
||||
var histServChan chan struct{} // Will be initialized only if the server starts
|
||||
if cfg.HistoryServerEnabled {
|
||||
histServChan = make(chan struct{})
|
||||
rpcWait = append(rpcWait, histServChan)
|
||||
go startHistoryServer(histServChan)
|
||||
}
|
||||
|
||||
if cfg.HistoryAgentEnabled {
|
||||
engine.Logger.Info("Starting CGRateS History Agent.")
|
||||
go startHistoryAgent(scribeServer, histServChan)
|
||||
}
|
||||
|
||||
var medChan chan struct{}
|
||||
if cfg.MediatorEnabled {
|
||||
engine.Logger.Info("Starting CGRateS Mediator service.")
|
||||
medChan = make(chan struct{})
|
||||
go startMediator(responder, logDb, cdrDb, cacheChan, medChan)
|
||||
}
|
||||
|
||||
var cdrsChan chan struct{}
|
||||
if cfg.CDRSEnabled {
|
||||
engine.Logger.Info("Starting CGRateS CDRS service.")
|
||||
cdrsChan = make(chan struct{})
|
||||
httpWait = append(httpWait, cdrsChan)
|
||||
go startCDRS(responder, cdrDb, medChan, cdrsChan)
|
||||
}
|
||||
|
||||
if cfg.SMEnabled {
|
||||
engine.Logger.Info("Starting CGRateS SessionManager.")
|
||||
go startSessionManager(responder, logDb)
|
||||
engine.Logger.Info("Starting CGRateS SessionManager service.")
|
||||
go startSessionManager(responder, logDb, cacheChan)
|
||||
// close all sessions on shutdown
|
||||
go shutdownSessionmanagerSingnalHandler()
|
||||
}
|
||||
|
||||
if cfg.MediatorEnabled {
|
||||
engine.Logger.Info("Starting CGRateS Mediator.")
|
||||
go startMediator(responder, logDb, cdrDb)
|
||||
}
|
||||
|
||||
if cfg.CDRSEnabled {
|
||||
engine.Logger.Info("Starting CGRateS CDR Server.")
|
||||
go startCDRS(responder, cdrDb)
|
||||
}
|
||||
|
||||
if cfg.HistoryServerEnabled || cfg.HistoryAgentEnabled {
|
||||
engine.Logger.Info("Starting History Service.")
|
||||
go startHistoryScribe()
|
||||
}
|
||||
if cfg.CdrcEnabled {
|
||||
engine.Logger.Info("Starting CGRateS CDR Client.")
|
||||
go startCdrc()
|
||||
engine.Logger.Info("Starting CGRateS CDR client.")
|
||||
go startCdrc(cdrsChan)
|
||||
}
|
||||
|
||||
// Start the servers
|
||||
go serveRpc(rpcWait)
|
||||
go serveHttp(httpWait)
|
||||
|
||||
<-exitChan
|
||||
|
||||
if *pidFile != "" {
|
||||
if err := os.Remove(*pidFile); err != nil {
|
||||
engine.Logger.Warning("Could not remove pid file: " + err.Error())
|
||||
|
||||
@@ -76,7 +76,7 @@ func unregisterFromBalancer() {
|
||||
}
|
||||
var reply int
|
||||
engine.Logger.Info(fmt.Sprintf("Unregistering from balancer %s", cfg.RaterBalancer))
|
||||
client.Call("Responder.UnRegisterRater", cfg.RaterListen, &reply)
|
||||
client.Call("Responder.UnRegisterRater", cfg.RPCGOBListen, &reply)
|
||||
if err := client.Close(); err != nil {
|
||||
engine.Logger.Crit("Could not close balancer unregistration!")
|
||||
exitChan <- true
|
||||
@@ -95,7 +95,7 @@ func registerToBalancer() {
|
||||
}
|
||||
var reply int
|
||||
engine.Logger.Info(fmt.Sprintf("Registering to balancer %s", cfg.RaterBalancer))
|
||||
client.Call("Responder.RegisterRater", cfg.RaterListen, &reply)
|
||||
client.Call("Responder.RegisterRater", cfg.RPCGOBListen, &reply)
|
||||
if err := client.Close(); err != nil {
|
||||
engine.Logger.Crit("Could not close balancer registration!")
|
||||
exitChan <- true
|
||||
|
||||
@@ -19,12 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"path"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
@@ -35,7 +33,7 @@ import (
|
||||
|
||||
var (
|
||||
//separator = flag.String("separator", ",", "Default field separator")
|
||||
cgrConfig, _ = config.NewDefaultCGRConfig()
|
||||
cgrConfig, _ = config.NewDefaultCGRConfig()
|
||||
ratingdb_type = flag.String("ratingdb_type", cgrConfig.RatingDBType, "The type of the RatingDb database <redis>")
|
||||
ratingdb_host = flag.String("ratingdb_host", cgrConfig.RatingDBHost, "The RatingDb host to connect to.")
|
||||
ratingdb_port = flag.String("ratingdb_port", cgrConfig.RatingDBPort, "The RatingDb port to bind to.")
|
||||
@@ -68,9 +66,8 @@ var (
|
||||
stats = flag.Bool("stats", false, "Generates statsistics about given data.")
|
||||
fromStorDb = flag.Bool("from_stordb", false, "Load the tariff plan from storDb to dataDb")
|
||||
toStorDb = flag.Bool("to_stordb", false, "Import the tariff plan from files to storDb")
|
||||
historyServer = flag.String("history_server", cgrConfig.HistoryServer, "The history server address:port, empty to disable automaticautomatic history archiving")
|
||||
raterAddress = flag.String("rater_address", cgrConfig.MediatorRater, "Rater service to contact for cache reloads, empty to disable automatic cache reloads")
|
||||
rpcEncoding = flag.String("rpc_encoding", cgrConfig.RPCEncoding, "The history server rpc encoding json|gob")
|
||||
historyServer = flag.String("history_server", cgrConfig.RPCGOBListen, "The history server address:port, empty to disable automaticautomatic history archiving")
|
||||
raterAddress = flag.String("rater_address", cgrConfig.RPCGOBListen, "Rater service to contact for cache reloads, empty to disable automatic cache reloads")
|
||||
runId = flag.String("runid", "", "Uniquely identify an import/load, postpended to some automatic fields")
|
||||
)
|
||||
|
||||
@@ -82,21 +79,21 @@ func main() {
|
||||
}
|
||||
var errRatingDb, errAccDb, errStorDb, err error
|
||||
var ratingDb engine.RatingStorage
|
||||
var accountDb engine.AccountingStorage
|
||||
var accountDb engine.AccountingStorage
|
||||
var storDb engine.LoadStorage
|
||||
var rater *rpc.Client
|
||||
var loader engine.TPLoader
|
||||
// Init necessary db connections, only if not already
|
||||
if !*dryRun { // make sure we do not need db connections on dry run, also not importing into any stordb
|
||||
if *fromStorDb {
|
||||
ratingDb, errRatingDb = engine.ConfigureRatingStorage(*ratingdb_type, *ratingdb_host, *ratingdb_port, *ratingdb_name,
|
||||
ratingDb, errRatingDb = engine.ConfigureRatingStorage(*ratingdb_type, *ratingdb_host, *ratingdb_port, *ratingdb_name,
|
||||
*ratingdb_user, *ratingdb_pass, *dbdata_encoding)
|
||||
accountDb, errAccDb = engine.ConfigureAccountingStorage(*accountdb_type, *accountdb_host, *accountdb_port, *accountdb_name, *accountdb_user, *accountdb_pass, *dbdata_encoding)
|
||||
storDb, errStorDb = engine.ConfigureLoadStorage(*stor_db_type, *stor_db_host, *stor_db_port, *stor_db_name, *stor_db_user, *stor_db_pass, *dbdata_encoding)
|
||||
} else if *toStorDb { // Import from csv files to storDb
|
||||
storDb, errStorDb = engine.ConfigureLoadStorage(*stor_db_type, *stor_db_host, *stor_db_port, *stor_db_name, *stor_db_user, *stor_db_pass, *dbdata_encoding)
|
||||
} else { // Default load from csv files to dataDb
|
||||
ratingDb, errRatingDb = engine.ConfigureRatingStorage(*ratingdb_type, *ratingdb_host, *ratingdb_port, *ratingdb_name,
|
||||
ratingDb, errRatingDb = engine.ConfigureRatingStorage(*ratingdb_type, *ratingdb_host, *ratingdb_port, *ratingdb_name,
|
||||
*ratingdb_user, *ratingdb_pass, *dbdata_encoding)
|
||||
accountDb, errAccDb = engine.ConfigureAccountingStorage(*accountdb_type, *accountdb_host, *accountdb_port, *accountdb_name, *accountdb_user, *accountdb_pass, *dbdata_encoding)
|
||||
}
|
||||
@@ -132,18 +129,19 @@ func main() {
|
||||
log.Fatal(err, "\n\t", v.Message)
|
||||
}
|
||||
}
|
||||
loader = engine.NewFileCSVReader(ratingDb, accountDb, ',',
|
||||
path.Join(*dataPath, utils.DESTINATIONS_CSV),
|
||||
path.Join(*dataPath, utils.TIMINGS_CSV),
|
||||
path.Join(*dataPath, utils.RATES_CSV),
|
||||
path.Join(*dataPath, utils.DESTINATION_RATES_CSV),
|
||||
path.Join(*dataPath, utils.RATING_PLANS_CSV),
|
||||
path.Join(*dataPath, utils.RATING_PROFILES_CSV),
|
||||
path.Join(*dataPath, utils.ACTIONS_CSV),
|
||||
path.Join(*dataPath, utils.ACTION_TIMINGS_CSV),
|
||||
path.Join(*dataPath, utils.ACTION_TRIGGERS_CSV),
|
||||
path.Join(*dataPath, utils.ACCOUNT_ACTIONS_CSV))
|
||||
}
|
||||
loader = engine.NewFileCSVReader(ratingDb, accountDb, ',',
|
||||
path.Join(*dataPath, utils.DESTINATIONS_CSV),
|
||||
path.Join(*dataPath, utils.TIMINGS_CSV),
|
||||
path.Join(*dataPath, utils.RATES_CSV),
|
||||
path.Join(*dataPath, utils.DESTINATION_RATES_CSV),
|
||||
path.Join(*dataPath, utils.RATING_PLANS_CSV),
|
||||
path.Join(*dataPath, utils.RATING_PROFILES_CSV),
|
||||
path.Join(*dataPath, utils.SHARED_GROUPS_CSV),
|
||||
path.Join(*dataPath, utils.ACTIONS_CSV),
|
||||
path.Join(*dataPath, utils.ACTION_PLANS_CSV),
|
||||
path.Join(*dataPath, utils.ACTION_TRIGGERS_CSV),
|
||||
path.Join(*dataPath, utils.ACCOUNT_ACTIONS_CSV))
|
||||
}
|
||||
err = loader.LoadAll()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -155,23 +153,18 @@ func main() {
|
||||
return
|
||||
}
|
||||
if *historyServer != "" { // Init scribeAgent so we can store the differences
|
||||
if scribeAgent, err := history.NewProxyScribe(*historyServer, *rpcEncoding); err != nil {
|
||||
if scribeAgent, err := history.NewProxyScribe(*historyServer); err != nil {
|
||||
log.Fatalf("Could not connect to history server, error: %s. Make sure you have properly configured it via -history_server flag.", err.Error())
|
||||
return
|
||||
} else {
|
||||
engine.SetHistoryScribe(scribeAgent)
|
||||
gob.Register(&engine.Destination{})
|
||||
defer scribeAgent.Client.Close()
|
||||
}
|
||||
} else {
|
||||
log.Print("WARNING: Rates history archiving is disabled!")
|
||||
}
|
||||
if *raterAddress != "" { // Init connection to rater so we can reload it's data
|
||||
if *rpcEncoding == config.JSON {
|
||||
rater, err = jsonrpc.Dial("tcp", *raterAddress)
|
||||
} else {
|
||||
rater, err = rpc.Dial("tcp", *raterAddress)
|
||||
}
|
||||
rater, err = rpc.Dial("tcp", *raterAddress)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to rater: %s", err.Error())
|
||||
return
|
||||
@@ -194,11 +187,14 @@ func main() {
|
||||
rplIds, _ := loader.GetLoadedIds(engine.RATING_PLAN_PREFIX)
|
||||
rpfIds, _ := loader.GetLoadedIds(engine.RATING_PROFILE_PREFIX)
|
||||
actIds, _ := loader.GetLoadedIds(engine.ACTION_PREFIX)
|
||||
shgIds, _ := loader.GetLoadedIds(engine.SHARED_GROUP_PREFIX)
|
||||
rpAliases, _ := loader.GetLoadedIds(engine.RP_ALIAS_PREFIX)
|
||||
accAliases, _ := loader.GetLoadedIds(engine.ACC_ALIAS_PREFIX)
|
||||
// Reload cache first since actions could be calling info from within
|
||||
if *verbose {
|
||||
log.Print("Reloading cache")
|
||||
}
|
||||
if err = rater.Call("ApierV1.ReloadCache", utils.ApiReloadCache{dstIds, rplIds, rpfIds, actIds}, &reply); err != nil {
|
||||
if err = rater.Call("ApierV1.ReloadCache", utils.ApiReloadCache{dstIds, rplIds, rpfIds, actIds, shgIds, rpAliases, accAliases}, &reply); err != nil {
|
||||
log.Fatalf("Got error on cache reload: %s", err.Error())
|
||||
}
|
||||
actTmgIds, _ := loader.GetLoadedIds(engine.ACTION_TIMING_PREFIX)
|
||||
|
||||
@@ -20,48 +20,47 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
"fmt"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
|
||||
"github.com/cgrates/cgrates/config"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
)
|
||||
|
||||
var (
|
||||
cgrConfig, _ = config.NewDefaultCGRConfig()
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
runs = flag.Int("runs", 10000, "stress cycle number")
|
||||
parallel = flag.Int("parallel", 0, "run n requests in parallel")
|
||||
ratingdb_type = flag.String("ratingdb_type", cgrConfig.RatingDBType, "The type of the RatingDb database <redis>")
|
||||
ratingdb_host = flag.String("ratingdb_host", cgrConfig.RatingDBHost, "The RatingDb host to connect to.")
|
||||
ratingdb_port = flag.String("ratingdb_port", cgrConfig.RatingDBPort, "The RatingDb port to bind to.")
|
||||
ratingdb_name = flag.String("ratingdb_name", cgrConfig.RatingDBName, "The name/number of the RatingDb to connect to.")
|
||||
ratingdb_user = flag.String("ratingdb_user", cgrConfig.RatingDBUser, "The RatingDb user to sign in as.")
|
||||
ratingdb_pass = flag.String("ratingdb_passwd", cgrConfig.RatingDBPass, "The RatingDb user's password.")
|
||||
accountdb_type = flag.String("accountdb_type", cgrConfig.AccountDBType, "The type of the AccountingDb database <redis>")
|
||||
accountdb_host = flag.String("accountdb_host", cgrConfig.AccountDBHost, "The AccountingDb host to connect to.")
|
||||
accountdb_port = flag.String("accountdb_port", cgrConfig.AccountDBPort, "The AccountingDb port to bind to.")
|
||||
accountdb_name = flag.String("accountdb_name", cgrConfig.AccountDBName, "The name/number of the AccountingDb to connect to.")
|
||||
accountdb_user = flag.String("accountdb_user", cgrConfig.AccountDBUser, "The AccountingDb user to sign in as.")
|
||||
accountdb_pass = flag.String("accountdb_passwd", cgrConfig.AccountDBPass, "The AccountingDb user's password.")
|
||||
cgrConfig, _ = config.NewDefaultCGRConfig()
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
runs = flag.Int("runs", 10000, "stress cycle number")
|
||||
parallel = flag.Int("parallel", 0, "run n requests in parallel")
|
||||
ratingdb_type = flag.String("ratingdb_type", cgrConfig.RatingDBType, "The type of the RatingDb database <redis>")
|
||||
ratingdb_host = flag.String("ratingdb_host", cgrConfig.RatingDBHost, "The RatingDb host to connect to.")
|
||||
ratingdb_port = flag.String("ratingdb_port", cgrConfig.RatingDBPort, "The RatingDb port to bind to.")
|
||||
ratingdb_name = flag.String("ratingdb_name", cgrConfig.RatingDBName, "The name/number of the RatingDb to connect to.")
|
||||
ratingdb_user = flag.String("ratingdb_user", cgrConfig.RatingDBUser, "The RatingDb user to sign in as.")
|
||||
ratingdb_pass = flag.String("ratingdb_passwd", cgrConfig.RatingDBPass, "The RatingDb user's password.")
|
||||
accountdb_type = flag.String("accountdb_type", cgrConfig.AccountDBType, "The type of the AccountingDb database <redis>")
|
||||
accountdb_host = flag.String("accountdb_host", cgrConfig.AccountDBHost, "The AccountingDb host to connect to.")
|
||||
accountdb_port = flag.String("accountdb_port", cgrConfig.AccountDBPort, "The AccountingDb port to bind to.")
|
||||
accountdb_name = flag.String("accountdb_name", cgrConfig.AccountDBName, "The name/number of the AccountingDb to connect to.")
|
||||
accountdb_user = flag.String("accountdb_user", cgrConfig.AccountDBUser, "The AccountingDb user to sign in as.")
|
||||
accountdb_pass = flag.String("accountdb_passwd", cgrConfig.AccountDBPass, "The AccountingDb user's password.")
|
||||
dbdata_encoding = flag.String("dbdata_encoding", cgrConfig.DBDataEncoding, "The encoding used to store object data in strings.")
|
||||
raterAddress = flag.String("rater_address", "", "Rater address for remote tests. Empty for internal rater.")
|
||||
rpcEncoding = flag.String("rpc_encoding", cgrConfig.RPCEncoding, "Rpc encoding to use when talking to remote rater <json|gob>")
|
||||
tor = flag.String("tor", "call", "The type of record to use in queries.")
|
||||
tenant = flag.String("tenant", "call", "The type of record to use in queries.")
|
||||
subject = flag.String("subject", "1001", "The rating subject to use in queries.")
|
||||
destination = flag.String("destination", "+4986517174963", "The destination to use in queries.")
|
||||
|
||||
raterAddress = flag.String("rater_address", "", "Rater address for remote tests. Empty for internal rater.")
|
||||
tor = flag.String("tor", "call", "The type of record to use in queries.")
|
||||
tenant = flag.String("tenant", "call", "The type of record to use in queries.")
|
||||
subject = flag.String("subject", "1001", "The rating subject to use in queries.")
|
||||
destination = flag.String("destination", "+4986517174963", "The destination to use in queries.")
|
||||
|
||||
nilDuration = time.Duration(0)
|
||||
)
|
||||
|
||||
func durInternalRater( cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
func durInternalRater(cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
ratingDb, err := engine.ConfigureRatingStorage(*ratingdb_type, *ratingdb_host, *ratingdb_port, *ratingdb_name, *ratingdb_user, *ratingdb_pass, *dbdata_encoding)
|
||||
if err != nil {
|
||||
return nilDuration, fmt.Errorf("Could not connect to rating database: %s", err.Error())
|
||||
@@ -74,7 +73,7 @@ func durInternalRater( cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
}
|
||||
defer accountDb.Close()
|
||||
engine.SetAccountingStorage(accountDb)
|
||||
if err := ratingDb.CacheRating(nil, nil, nil); err != nil {
|
||||
if err := ratingDb.CacheRating(nil, nil, nil, nil); err != nil {
|
||||
return nilDuration, fmt.Errorf("Cache rating error: %s", err.Error())
|
||||
}
|
||||
log.Printf("Runnning %d cycles...", *runs)
|
||||
@@ -104,16 +103,9 @@ func durInternalRater( cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
return time.Since(start), nil
|
||||
}
|
||||
|
||||
|
||||
func durRemoteRater( cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
func durRemoteRater(cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
result := engine.CallCost{}
|
||||
var client *rpc.Client
|
||||
var err error
|
||||
if *rpcEncoding=="json" {
|
||||
client, err = jsonrpc.Dial("tcp", *raterAddress)
|
||||
} else {
|
||||
client, err = rpc.Dial("tcp", *raterAddress)
|
||||
}
|
||||
client, err := rpc.Dial("tcp", *raterAddress)
|
||||
if err != nil {
|
||||
return nilDuration, fmt.Errorf("Could not connect to engine: ", err.Error())
|
||||
}
|
||||
@@ -144,9 +136,6 @@ func durRemoteRater( cd *engine.CallDescriptor) (time.Duration, error) {
|
||||
log.Println(result)
|
||||
return time.Since(start), nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
430
config/config.go
430
config/config.go
@@ -21,6 +21,8 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/goconf/conf"
|
||||
@@ -29,7 +31,6 @@ import (
|
||||
|
||||
const (
|
||||
DISABLED = "disabled"
|
||||
INTERNAL = "internal"
|
||||
JSON = "json"
|
||||
GOB = "gob"
|
||||
POSTGRES = "postgres"
|
||||
@@ -60,86 +61,102 @@ type CGRConfig struct {
|
||||
RatingDBUser string // The user to sign in as.
|
||||
RatingDBPass string // The user's password.
|
||||
AccountDBType string
|
||||
AccountDBHost string // The host to connect to. Values that start with / are for UNIX domain sockets.
|
||||
AccountDBPort string // The port to bind to.
|
||||
AccountDBName string // The name of the database to connect to.
|
||||
AccountDBUser string // The user to sign in as.
|
||||
AccountDBPass string // The user's password.
|
||||
StorDBType string // Should reflect the database type used to store logs
|
||||
StorDBHost string // The host to connect to. Values that start with / are for UNIX domain sockets.
|
||||
StorDBPort string // Th e port to bind to.
|
||||
StorDBName string // The name of the database to connect to.
|
||||
StorDBUser string // The user to sign in as.
|
||||
StorDBPass string // The user's password.
|
||||
DBDataEncoding string // The encoding used to store object data in strings: <msgpack|json>
|
||||
RPCEncoding string // RPC encoding used on APIs: <gob|json>.
|
||||
DefaultReqType string // Use this request type if not defined on top
|
||||
DefaultTOR string // set default type of record
|
||||
DefaultTenant string // set default tenant
|
||||
DefaultSubject string // set default rating subject, useful in case of fallback
|
||||
RoundingMethod string // Rounding method for the end price: <*up|*middle|*down>
|
||||
RoundingDecimals int // Number of decimals to round end prices at
|
||||
RaterEnabled bool // start standalone server (no balancer)
|
||||
RaterBalancer string // balancer address host:port
|
||||
RaterListen string // listening address host:port
|
||||
AccountDBHost string // The host to connect to. Values that start with / are for UNIX domain sockets.
|
||||
AccountDBPort string // The port to bind to.
|
||||
AccountDBName string // The name of the database to connect to.
|
||||
AccountDBUser string // The user to sign in as.
|
||||
AccountDBPass string // The user's password.
|
||||
StorDBType string // Should reflect the database type used to store logs
|
||||
StorDBHost string // The host to connect to. Values that start with / are for UNIX domain sockets.
|
||||
StorDBPort string // Th e port to bind to.
|
||||
StorDBName string // The name of the database to connect to.
|
||||
StorDBUser string // The user to sign in as.
|
||||
StorDBPass string // The user's password.
|
||||
DBDataEncoding string // The encoding used to store object data in strings: <msgpack|json>
|
||||
RPCJSONListen string // RPC JSON listening address
|
||||
RPCGOBListen string // RPC GOB listening address
|
||||
HTTPListen string // HTTP listening address
|
||||
DefaultReqType string // Use this request type if not defined on top
|
||||
DefaultTOR string // set default type of record
|
||||
DefaultTenant string // set default tenant
|
||||
DefaultSubject string // set default rating subject, useful in case of fallback
|
||||
RoundingMethod string // Rounding method for the end price: <*up|*middle|*down>
|
||||
RoundingDecimals int // Number of decimals to round end prices at
|
||||
XmlCfgDocument *CgrXmlCfgDocument // Load additional configuration inside xml document
|
||||
RaterEnabled bool // start standalone server (no balancer)
|
||||
RaterBalancer string // balancer address host:port
|
||||
BalancerEnabled bool
|
||||
BalancerListen string // Json RPC server address
|
||||
SchedulerEnabled bool
|
||||
CDRSEnabled bool // Enable CDR Server service
|
||||
CDRSListen string // CDRS's listening interface: <x.y.z.y:1234>.
|
||||
CDRSExtraFields []string //Extra fields to store in CDRs
|
||||
CDRSMediator string // Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
|
||||
CdreCdrFormat string // Format of the exported CDRs. <csv>
|
||||
CdreExtraFields []string // Extra fields list to add in exported CDRs
|
||||
CdreDir string // Path towards exported cdrs directory
|
||||
CdrcEnabled bool // Enable CDR client functionality
|
||||
CdrcCdrs string // Address where to reach CDR server
|
||||
CdrcCdrsMethod string // Mechanism to use when posting CDRs on server <http_cgr>
|
||||
CdrcRunDelay time.Duration // Sleep interval between consecutive runs, if time unit missing, defaults to seconds, 0 to use automation via inotify
|
||||
CdrcCdrType string // CDR file format <csv>.
|
||||
CdrcCdrInDir string // Absolute path towards the directory where the CDRs are stored.
|
||||
CdrcCdrOutDir string // Absolute path towards the directory where processed CDRs will be moved.
|
||||
CdrcSourceId string // Tag identifying the source of the CDRs within CGRS database.
|
||||
CdrcAccIdField string // Accounting id field identifier. Use index number in case of .csv cdrs.
|
||||
CdrcReqTypeField string // Request type field identifier. Use index number in case of .csv cdrs.
|
||||
CdrcDirectionField string // Direction field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcTenantField string // Tenant field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcTorField string // Type of Record field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcAccountField string // Account field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcSubjectField string // Subject field identifier. Use index numbers in case of .csv CDRs.
|
||||
CdrcDestinationField string // Destination field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcAnswerTimeField string // Answer time field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcDurationField string // Duration field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcExtraFields []string // Field identifiers of the fields to add in extra fields section, special format in case of .csv "field1:index1,field2:index2"
|
||||
CDRSEnabled bool // Enable CDR Server service
|
||||
CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs
|
||||
CDRSMediator string // Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
|
||||
CdreCdrFormat string // Format of the exported CDRs. <csv>
|
||||
CdreDir string // Path towards exported cdrs directory
|
||||
CdreExportedFields []*utils.RSRField // List of fields in the exported CDRs
|
||||
CdreFWXmlTemplate *CgrXmlCdreFwCfg // Use this configuration as export template in case of fixed fields length
|
||||
CdrcEnabled bool // Enable CDR client functionality
|
||||
CdrcCdrs string // Address where to reach CDR server
|
||||
CdrcCdrsMethod string // Mechanism to use when posting CDRs on server <http_cgr>
|
||||
CdrcRunDelay time.Duration // Sleep interval between consecutive runs, 0 to use automation via inotify
|
||||
CdrcCdrType string // CDR file format <csv>.
|
||||
CdrcCdrInDir string // Absolute path towards the directory where the CDRs are stored.
|
||||
CdrcCdrOutDir string // Absolute path towards the directory where processed CDRs will be moved.
|
||||
CdrcSourceId string // Tag identifying the source of the CDRs within CGRS database.
|
||||
CdrcAccIdField string // Accounting id field identifier. Use index number in case of .csv cdrs.
|
||||
CdrcReqTypeField string // Request type field identifier. Use index number in case of .csv cdrs.
|
||||
CdrcDirectionField string // Direction field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcTenantField string // Tenant field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcTorField string // Type of Record field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcAccountField string // Account field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcSubjectField string // Subject field identifier. Use index numbers in case of .csv CDRs.
|
||||
CdrcDestinationField string // Destination field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcSetupTimeField string // Setup time field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcAnswerTimeField string // Answer time field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcDurationField string // Duration field identifier. Use index numbers in case of .csv cdrs.
|
||||
CdrcExtraFields []string // Extra fields to extract, special format in case of .csv "field1:index1,field2:index2"
|
||||
SMEnabled bool
|
||||
SMSwitchType string
|
||||
SMRater string // address where to access rater. Can be internal, direct rater address or the address of a balancer
|
||||
SMRaterReconnects int // Number of reconnect attempts to rater
|
||||
SMDebitInterval int // the period to be debited in advanced during a call (in seconds)
|
||||
SMRater string // address where to access rater. Can be internal, direct rater address or the address of a balancer
|
||||
SMRaterReconnects int // Number of reconnect attempts to rater
|
||||
SMDebitInterval int // the period to be debited in advanced during a call (in seconds)
|
||||
SMMaxCallDuration time.Duration // The maximum duration of a call
|
||||
MediatorEnabled bool // Starts Mediator service: <true|false>.
|
||||
MediatorListen string // Mediator's listening interface: <internal>.
|
||||
MediatorRater string // Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
MediatorRaterReconnects int // Number of reconnects to rater before giving up.
|
||||
MediatorRunIds []string // Identifiers for each mediation run on CDRs
|
||||
MediatorReqTypeFields []string // Name of request type fields to be used during mediation. Use index number in case of .csv cdrs.
|
||||
MediatorDirectionFields []string // Name of direction fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorTenantFields []string // Name of tenant fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorTORFields []string // Name of tor fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorAccountFields []string // Name of account fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorSubjectFields []string // Name of subject fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorDestFields []string // Name of destination fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorAnswerTimeFields []string // Name of time_start fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorDurationFields []string // Name of duration fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
FreeswitchServer string // freeswitch address host:port
|
||||
FreeswitchPass string // FS socket password
|
||||
FreeswitchReconnects int // number of times to attempt reconnect after connect fails
|
||||
HistoryAgentEnabled bool // Starts History as an agent: <true|false>.
|
||||
HistoryServer string // Address where to reach the master history server: <internal|x.y.z.y:1234>
|
||||
HistoryServerEnabled bool // Starts History as server: <true|false>.
|
||||
HistoryListen string // History server listening interface: <internal|x.y.z.y:1234>
|
||||
HistoryDir string // Location on disk where to store history files.
|
||||
HistorySaveInterval time.Duration // The timout duration between history writes
|
||||
SMRunIds []string // Identifiers of additional sessions control.
|
||||
SMReqTypeFields []string // Name of request type fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMDirectionFields []string // Name of direction fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMTenantFields []string // Name of tenant fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMTORFields []string // Name of tor fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMAccountFields []string // Name of account fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMSubjectFields []string // Name of fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMDestFields []string // Name of destination fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMSetupTimeFields []string // Name of setup_time fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMAnswerTimeFields []string // Name of answer_time fields to be used during additional sessions control <""|*default|field_name>.
|
||||
SMDurationFields []string // Name of duration fields to be used during additional sessions control <""|*default|field_name>.
|
||||
MediatorEnabled bool // Starts Mediator service: <true|false>.
|
||||
MediatorRater string // Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
MediatorRaterReconnects int // Number of reconnects to rater before giving up.
|
||||
MediatorRunIds []string // Identifiers for each mediation run on CDRs
|
||||
MediatorReqTypeFields []string // Name of request type fields to be used during mediation. Use index number in case of .csv cdrs.
|
||||
MediatorDirectionFields []string // Name of direction fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorTenantFields []string // Name of tenant fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorTORFields []string // Name of tor fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorAccountFields []string // Name of account fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorSubjectFields []string // Name of subject fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorDestFields []string // Name of destination fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorSetupTimeFields []string // Name of setup_time fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorAnswerTimeFields []string // Name of answer_time fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
MediatorDurationFields []string // Name of duration fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
FreeswitchServer string // freeswitch address host:port
|
||||
FreeswitchPass string // FS socket password
|
||||
FreeswitchReconnects int // number of times to attempt reconnect after connect fails
|
||||
HistoryAgentEnabled bool // Starts History as an agent: <true|false>.
|
||||
HistoryServer string // Address where to reach the master history server: <internal|x.y.z.y:1234>
|
||||
HistoryServerEnabled bool // Starts History as server: <true|false>.
|
||||
HistoryDir string // Location on disk where to store history files.
|
||||
HistorySaveInterval time.Duration // The timout duration between history writes
|
||||
MailerServer string // The server to use when sending emails out
|
||||
MailerAuthUser string // Authenticate to email server using this user
|
||||
MailerAuthPass string // Authenticate to email server with this password
|
||||
MailerFromAddr string // From address used when sending emails out
|
||||
}
|
||||
|
||||
func (self *CGRConfig) setDefaults() error {
|
||||
@@ -162,28 +179,27 @@ func (self *CGRConfig) setDefaults() error {
|
||||
self.StorDBUser = "cgrates"
|
||||
self.StorDBPass = "CGRateS.org"
|
||||
self.DBDataEncoding = utils.MSGPACK
|
||||
self.RPCEncoding = JSON
|
||||
self.RPCJSONListen = "127.0.0.1:2012"
|
||||
self.RPCGOBListen = "127.0.0.1:2013"
|
||||
self.HTTPListen = "127.0.0.1:2080"
|
||||
self.DefaultReqType = utils.RATED
|
||||
self.DefaultTOR = "call"
|
||||
self.DefaultTenant = "cgrates.org"
|
||||
self.DefaultSubject = "cgrates"
|
||||
self.RoundingMethod = utils.ROUNDING_MIDDLE
|
||||
self.RoundingDecimals = 4
|
||||
self.XmlCfgDocument = nil
|
||||
self.RaterEnabled = false
|
||||
self.RaterBalancer = DISABLED
|
||||
self.RaterListen = "127.0.0.1:2012"
|
||||
self.RaterBalancer = ""
|
||||
self.BalancerEnabled = false
|
||||
self.BalancerListen = "127.0.0.1:2013"
|
||||
self.SchedulerEnabled = false
|
||||
self.CDRSEnabled = false
|
||||
self.CDRSListen = "127.0.0.1:2022"
|
||||
self.CDRSExtraFields = []string{}
|
||||
self.CDRSExtraFields = []*utils.RSRField{}
|
||||
self.CDRSMediator = ""
|
||||
self.CdreCdrFormat = "csv"
|
||||
self.CdreExtraFields = []string{}
|
||||
self.CdreDir = "/var/log/cgrates/cdr/cdrexport/csv"
|
||||
self.CdrcEnabled = false
|
||||
self.CdrcCdrs = "127.0.0.1:2022"
|
||||
self.CdrcCdrs = utils.INTERNAL
|
||||
self.CdrcCdrsMethod = "http_cgr"
|
||||
self.CdrcRunDelay = time.Duration(0)
|
||||
self.CdrcCdrType = "csv"
|
||||
@@ -198,12 +214,12 @@ func (self *CGRConfig) setDefaults() error {
|
||||
self.CdrcAccountField = "5"
|
||||
self.CdrcSubjectField = "6"
|
||||
self.CdrcDestinationField = "7"
|
||||
self.CdrcAnswerTimeField = "8"
|
||||
self.CdrcDurationField = "9"
|
||||
self.CdrcSetupTimeField = "8"
|
||||
self.CdrcAnswerTimeField = "9"
|
||||
self.CdrcDurationField = "10"
|
||||
self.CdrcExtraFields = []string{}
|
||||
self.MediatorEnabled = false
|
||||
self.MediatorListen = "127.0.0.1:2032"
|
||||
self.MediatorRater = "127.0.0.1:2012"
|
||||
self.MediatorRater = "internal"
|
||||
self.MediatorRaterReconnects = 3
|
||||
self.MediatorRunIds = []string{}
|
||||
self.MediatorSubjectFields = []string{}
|
||||
@@ -213,29 +229,102 @@ func (self *CGRConfig) setDefaults() error {
|
||||
self.MediatorTORFields = []string{}
|
||||
self.MediatorAccountFields = []string{}
|
||||
self.MediatorDestFields = []string{}
|
||||
self.MediatorSetupTimeFields = []string{}
|
||||
self.MediatorAnswerTimeFields = []string{}
|
||||
self.MediatorDurationFields = []string{}
|
||||
self.SMEnabled = false
|
||||
self.SMSwitchType = FS
|
||||
self.SMRater = "127.0.0.1:2012"
|
||||
self.SMRater = "internal"
|
||||
self.SMRaterReconnects = 3
|
||||
self.SMDebitInterval = 10
|
||||
self.SMMaxCallDuration = time.Duration(3) * time.Hour
|
||||
self.SMRunIds = []string{}
|
||||
self.SMReqTypeFields = []string{}
|
||||
self.SMDirectionFields = []string{}
|
||||
self.SMTenantFields = []string{}
|
||||
self.SMTORFields = []string{}
|
||||
self.SMAccountFields = []string{}
|
||||
self.SMSubjectFields = []string{}
|
||||
self.SMDestFields = []string{}
|
||||
self.SMSetupTimeFields = []string{}
|
||||
self.SMAnswerTimeFields = []string{}
|
||||
self.SMDurationFields = []string{}
|
||||
self.FreeswitchServer = "127.0.0.1:8021"
|
||||
self.FreeswitchPass = "ClueCon"
|
||||
self.FreeswitchReconnects = 5
|
||||
self.HistoryAgentEnabled = false
|
||||
self.HistoryServerEnabled = false
|
||||
self.HistoryServer = "127.0.0.1:2013"
|
||||
self.HistoryListen = "127.0.0.1:2013"
|
||||
self.HistoryServer = "internal"
|
||||
self.HistoryDir = "/var/log/cgrates/history"
|
||||
self.HistorySaveInterval = time.Duration(1) * time.Second
|
||||
self.MailerServer = "localhost:25"
|
||||
self.MailerAuthUser = "cgrates"
|
||||
self.MailerAuthPass = "CGRateS.org"
|
||||
self.MailerFromAddr = "cgr-mailer@localhost.localdomain"
|
||||
self.CdreExportedFields = []*utils.RSRField{
|
||||
&utils.RSRField{Id: utils.CGRID},
|
||||
&utils.RSRField{Id: utils.MEDI_RUNID},
|
||||
&utils.RSRField{Id: utils.ACCID},
|
||||
&utils.RSRField{Id: utils.CDRHOST},
|
||||
&utils.RSRField{Id: utils.REQTYPE},
|
||||
&utils.RSRField{Id: utils.DIRECTION},
|
||||
&utils.RSRField{Id: utils.TENANT},
|
||||
&utils.RSRField{Id: utils.TOR},
|
||||
&utils.RSRField{Id: utils.ACCOUNT},
|
||||
&utils.RSRField{Id: utils.SUBJECT},
|
||||
&utils.RSRField{Id: utils.DESTINATION},
|
||||
&utils.RSRField{Id: utils.SETUP_TIME},
|
||||
&utils.RSRField{Id: utils.ANSWER_TIME},
|
||||
&utils.RSRField{Id: utils.DURATION},
|
||||
&utils.RSRField{Id: utils.COST},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CGRConfig) checkConfigSanity() error {
|
||||
// Cdre sanity check for fixed_width
|
||||
if self.CdreCdrFormat == utils.CDRE_FIXED_WIDTH {
|
||||
if self.XmlCfgDocument == nil {
|
||||
return errors.New("Need XmlConfigurationDocument for fixed_width cdr export")
|
||||
} else if self.CdreFWXmlTemplate == nil {
|
||||
return errors.New("Need XmlTemplate for fixed_width cdr export")
|
||||
}
|
||||
}
|
||||
// SessionManager should have same fields config length for session emulation
|
||||
if len(self.SMReqTypeFields) != len(self.SMRunIds) ||
|
||||
len(self.SMDirectionFields) != len(self.SMRunIds) ||
|
||||
len(self.SMTenantFields) != len(self.SMRunIds) ||
|
||||
len(self.SMTORFields) != len(self.SMRunIds) ||
|
||||
len(self.SMAccountFields) != len(self.SMRunIds) ||
|
||||
len(self.SMSubjectFields) != len(self.SMRunIds) ||
|
||||
len(self.SMDestFields) != len(self.SMRunIds) ||
|
||||
len(self.SMSetupTimeFields) != len(self.SMRunIds) ||
|
||||
len(self.SMAnswerTimeFields) != len(self.SMRunIds) ||
|
||||
len(self.SMDurationFields) != len(self.SMRunIds) {
|
||||
return errors.New("<ConfigSanity> Inconsistent fields length for SessionManager session emulation")
|
||||
}
|
||||
// Mediator needs to have consistent extra fields definition
|
||||
if len(self.MediatorReqTypeFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorDirectionFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorTenantFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorTORFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorAccountFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorSubjectFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorDestFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorSetupTimeFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorAnswerTimeFields) != len(self.MediatorRunIds) ||
|
||||
len(self.MediatorDurationFields) != len(self.MediatorRunIds) {
|
||||
return errors.New("<ConfigSanity> Inconsistent fields length for Mediator extra fields")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDefaultCGRConfig() (*CGRConfig, error) {
|
||||
cfg := &CGRConfig{}
|
||||
cfg.setDefaults()
|
||||
if err := cfg.checkConfigSanity(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -245,7 +334,14 @@ func NewCGRConfig(cfgPath *string) (*CGRConfig, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Could not open the configuration file: %s", err))
|
||||
}
|
||||
return loadConfig(c)
|
||||
cfg, err := loadConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.checkConfigSanity(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func NewCGRConfigBytes(data []byte) (*CGRConfig, error) {
|
||||
@@ -253,7 +349,14 @@ func NewCGRConfigBytes(data []byte) (*CGRConfig, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Could not open the configuration file: %s", err))
|
||||
}
|
||||
return loadConfig(c)
|
||||
cfg, err := loadConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.checkConfigSanity(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
@@ -318,8 +421,14 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
if hasOpt = c.HasOption("global", "dbdata_encoding"); hasOpt {
|
||||
cfg.DBDataEncoding, _ = c.GetString("global", "dbdata_encoding")
|
||||
}
|
||||
if hasOpt = c.HasOption("global", "rpc_encoding"); hasOpt {
|
||||
cfg.RPCEncoding, _ = c.GetString("global", "rpc_encoding")
|
||||
if hasOpt = c.HasOption("global", "rpc_json_listen"); hasOpt {
|
||||
cfg.RPCJSONListen, _ = c.GetString("global", "rpc_json_listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("global", "rpc_gob_listen"); hasOpt {
|
||||
cfg.RPCGOBListen, _ = c.GetString("global", "rpc_gob_listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("global", "http_listen"); hasOpt {
|
||||
cfg.HTTPListen, _ = c.GetString("global", "http_listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("global", "default_reqtype"); hasOpt {
|
||||
cfg.DefaultReqType, _ = c.GetString("global", "default_reqtype")
|
||||
@@ -339,33 +448,40 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
if hasOpt = c.HasOption("global", "rounding_decimals"); hasOpt {
|
||||
cfg.RoundingDecimals, _ = c.GetInt("global", "rounding_decimals")
|
||||
}
|
||||
// XML config path defined, try loading the document
|
||||
if hasOpt = c.HasOption("global", "xmlcfg_path"); hasOpt {
|
||||
xmlCfgPath, _ := c.GetString("global", "xmlcfg_path")
|
||||
xmlFile, err := os.Open(xmlCfgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cgrXmlCfgDoc, err := ParseCgrXmlConfig(xmlFile); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
cfg.XmlCfgDocument = cgrXmlCfgDoc
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("rater", "enabled"); hasOpt {
|
||||
cfg.RaterEnabled, _ = c.GetBool("rater", "enabled")
|
||||
}
|
||||
if hasOpt = c.HasOption("rater", "balancer"); hasOpt {
|
||||
cfg.RaterBalancer, _ = c.GetString("rater", "balancer")
|
||||
}
|
||||
if hasOpt = c.HasOption("rater", "listen"); hasOpt {
|
||||
cfg.RaterListen, _ = c.GetString("rater", "listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("balancer", "enabled"); hasOpt {
|
||||
cfg.BalancerEnabled, _ = c.GetBool("balancer", "enabled")
|
||||
}
|
||||
if hasOpt = c.HasOption("balancer", "listen"); hasOpt {
|
||||
cfg.BalancerListen, _ = c.GetString("balancer", "listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("scheduler", "enabled"); hasOpt {
|
||||
cfg.SchedulerEnabled, _ = c.GetBool("scheduler", "enabled")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrs", "enabled"); hasOpt {
|
||||
cfg.CDRSEnabled, _ = c.GetBool("cdrs", "enabled")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrs", "listen"); hasOpt {
|
||||
cfg.CDRSListen, _ = c.GetString("cdrs", "listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrs", "extra_fields"); hasOpt {
|
||||
if cfg.CDRSExtraFields, errParse = ConfigSlice(c, "cdrs", "extra_fields"); errParse != nil {
|
||||
extraFieldsStr, _ := c.GetString("cdrs", "extra_fields")
|
||||
if extraFields, err := ParseRSRFields(extraFieldsStr); err != nil {
|
||||
return nil, errParse
|
||||
} else {
|
||||
cfg.CDRSExtraFields = extraFields
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrs", "mediator"); hasOpt {
|
||||
@@ -374,9 +490,20 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
if hasOpt = c.HasOption("cdre", "cdr_format"); hasOpt {
|
||||
cfg.CdreCdrFormat, _ = c.GetString("cdre", "cdr_format")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdre", "extra_fields"); hasOpt {
|
||||
if cfg.CdreExtraFields, errParse = ConfigSlice(c, "cdre", "extra_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
if hasOpt = c.HasOption("cdre", "export_template"); hasOpt { // Load configs for csv normally from template, fixed_width from xml file
|
||||
exportTemplate, _ := c.GetString("cdre", "export_template")
|
||||
if cfg.CdreCdrFormat != utils.CDRE_FIXED_WIDTH { // Csv most likely
|
||||
if extraFields, err := ParseRSRFields(exportTemplate); err != nil {
|
||||
return nil, errParse
|
||||
} else {
|
||||
cfg.CdreExportedFields = extraFields
|
||||
}
|
||||
} else if strings.HasPrefix(exportTemplate, utils.XML_PROFILE_PREFIX) {
|
||||
if xmlTemplate, err := cfg.XmlCfgDocument.GetCdreFWCfg(exportTemplate[len(utils.XML_PROFILE_PREFIX):]); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
cfg.CdreFWXmlTemplate = xmlTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("cdre", "export_dir"); hasOpt {
|
||||
@@ -392,7 +519,7 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
cfg.CdrcCdrsMethod, _ = c.GetString("cdrc", "cdrs_method")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrc", "run_delay"); hasOpt {
|
||||
durStr,_ := c.GetString("cdrc", "run_delay")
|
||||
durStr, _ := c.GetString("cdrc", "run_delay")
|
||||
if cfg.CdrcRunDelay, errParse = utils.ParseDurationWithSecs(durStr); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
@@ -433,6 +560,9 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
if hasOpt = c.HasOption("cdrc", "destination_field"); hasOpt {
|
||||
cfg.CdrcDestinationField, _ = c.GetString("cdrc", "destination_field")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrc", "setup_time_field"); hasOpt {
|
||||
cfg.CdrcSetupTimeField, _ = c.GetString("cdrc", "setup_time_field")
|
||||
}
|
||||
if hasOpt = c.HasOption("cdrc", "answer_time_field"); hasOpt {
|
||||
cfg.CdrcAnswerTimeField, _ = c.GetString("cdrc", "answer_time_field")
|
||||
}
|
||||
@@ -447,9 +577,6 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
if hasOpt = c.HasOption("mediator", "enabled"); hasOpt {
|
||||
cfg.MediatorEnabled, _ = c.GetBool("mediator", "enabled")
|
||||
}
|
||||
if hasOpt = c.HasOption("mediator", "listen"); hasOpt {
|
||||
cfg.MediatorListen, _ = c.GetString("mediator", "listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("mediator", "rater"); hasOpt {
|
||||
cfg.MediatorRater, _ = c.GetString("mediator", "rater")
|
||||
}
|
||||
@@ -496,6 +623,11 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("mediator", "setup_time_fields"); hasOpt {
|
||||
if cfg.MediatorSetupTimeFields, errParse = ConfigSlice(c, "mediator", "setup_time_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("mediator", "answer_time_fields"); hasOpt {
|
||||
if cfg.MediatorAnswerTimeFields, errParse = ConfigSlice(c, "mediator", "answer_time_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
@@ -522,11 +654,66 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
cfg.SMDebitInterval, _ = c.GetInt("session_manager", "debit_interval")
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "max_call_duration"); hasOpt {
|
||||
maxCallDurStr,_ := c.GetString("session_manager", "max_call_duration")
|
||||
maxCallDurStr, _ := c.GetString("session_manager", "max_call_duration")
|
||||
if cfg.SMMaxCallDuration, errParse = utils.ParseDurationWithSecs(maxCallDurStr); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "run_ids"); hasOpt {
|
||||
if cfg.SMRunIds, errParse = ConfigSlice(c, "session_manager", "run_ids"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "reqtype_fields"); hasOpt {
|
||||
if cfg.SMReqTypeFields, errParse = ConfigSlice(c, "session_manager", "reqtype_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "direction_fields"); hasOpt {
|
||||
if cfg.SMDirectionFields, errParse = ConfigSlice(c, "session_manager", "direction_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "tenant_fields"); hasOpt {
|
||||
if cfg.SMTenantFields, errParse = ConfigSlice(c, "session_manager", "tenant_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "tor_fields"); hasOpt {
|
||||
if cfg.SMTORFields, errParse = ConfigSlice(c, "session_manager", "tor_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "account_fields"); hasOpt {
|
||||
if cfg.SMAccountFields, errParse = ConfigSlice(c, "session_manager", "account_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "subject_fields"); hasOpt {
|
||||
if cfg.SMSubjectFields, errParse = ConfigSlice(c, "session_manager", "subject_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "destination_fields"); hasOpt {
|
||||
if cfg.SMDestFields, errParse = ConfigSlice(c, "session_manager", "destination_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "setup_time_fields"); hasOpt {
|
||||
if cfg.SMSetupTimeFields, errParse = ConfigSlice(c, "session_manager", "setup_time_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "answer_time_fields"); hasOpt {
|
||||
if cfg.SMAnswerTimeFields, errParse = ConfigSlice(c, "session_manager", "answer_time_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("session_manager", "duration_fields"); hasOpt {
|
||||
if cfg.SMDurationFields, errParse = ConfigSlice(c, "session_manager", "duration_fields"); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("freeswitch", "server"); hasOpt {
|
||||
cfg.FreeswitchServer, _ = c.GetString("freeswitch", "server")
|
||||
}
|
||||
@@ -545,17 +732,26 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
|
||||
if hasOpt = c.HasOption("history_server", "enabled"); hasOpt {
|
||||
cfg.HistoryServerEnabled, _ = c.GetBool("history_server", "enabled")
|
||||
}
|
||||
if hasOpt = c.HasOption("history_server", "listen"); hasOpt {
|
||||
cfg.HistoryListen, _ = c.GetString("history_server", "listen")
|
||||
}
|
||||
if hasOpt = c.HasOption("history_server", "history_dir"); hasOpt {
|
||||
cfg.HistoryDir, _ = c.GetString("history_server", "history_dir")
|
||||
}
|
||||
if hasOpt = c.HasOption("history_server", "save_interval"); hasOpt {
|
||||
saveIntvlStr,_ := c.GetString("history_server", "save_interval")
|
||||
saveIntvlStr, _ := c.GetString("history_server", "save_interval")
|
||||
if cfg.HistorySaveInterval, errParse = utils.ParseDurationWithSecs(saveIntvlStr); errParse != nil {
|
||||
return nil, errParse
|
||||
}
|
||||
}
|
||||
if hasOpt = c.HasOption("mailer", "server"); hasOpt {
|
||||
cfg.MailerServer, _ = c.GetString("mailer", "server")
|
||||
}
|
||||
if hasOpt = c.HasOption("mailer", "auth_user"); hasOpt {
|
||||
cfg.MailerAuthUser, _ = c.GetString("mailer", "auth_user")
|
||||
}
|
||||
if hasOpt = c.HasOption("mailer", "auth_passwd"); hasOpt {
|
||||
cfg.MailerAuthPass, _ = c.GetString("mailer", "auth_passwd")
|
||||
}
|
||||
if hasOpt = c.HasOption("mailer", "from_address"); hasOpt {
|
||||
cfg.MailerFromAddr, _ = c.GetString("mailer", "from_address")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
47
config/config_local_test.go
Normal file
47
config/config_local_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testLocal = flag.Bool("local", false, "Perform the tests only on local test environment, not by default.") // This flag will be passed here via "go test -local" args
|
||||
var dataDir = flag.String("data_dir", "/usr/share/cgrates", "CGR data dir path here")
|
||||
|
||||
func TestLoadXmlCfg(t *testing.T) {
|
||||
if !*testLocal {
|
||||
return
|
||||
}
|
||||
cfgPath := path.Join(*dataDir, "conf", "samples", "config_local_test.cfg")
|
||||
cfg, err := NewCGRConfig(&cfgPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if cfg.XmlCfgDocument == nil {
|
||||
t.Error("Did not load the XML Config Document")
|
||||
}
|
||||
if cdreFWCfg, err := cfg.XmlCfgDocument.GetCdreFWCfg("CDREFW-A"); err != nil {
|
||||
t.Error(err)
|
||||
} else if cdreFWCfg == nil {
|
||||
t.Error("Could not retrieve CDRExporter FixedWidth config instance")
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,7 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigSharing(t *testing.T) {
|
||||
cfg,_ := NewDefaultCGRConfig()
|
||||
cfg.RPCEncoding = utils.MSGPACK
|
||||
cfg, _ := NewDefaultCGRConfig()
|
||||
SetCgrConfig(cfg)
|
||||
cfgReturn := CgrConfig()
|
||||
if !reflect.DeepEqual(cfgReturn, cfg) {
|
||||
@@ -65,28 +64,27 @@ func TestDefaults(t *testing.T) {
|
||||
eCfg.StorDBUser = "cgrates"
|
||||
eCfg.StorDBPass = "CGRateS.org"
|
||||
eCfg.DBDataEncoding = utils.MSGPACK
|
||||
eCfg.RPCEncoding = JSON
|
||||
eCfg.RPCJSONListen = "127.0.0.1:2012"
|
||||
eCfg.RPCGOBListen = "127.0.0.1:2013"
|
||||
eCfg.HTTPListen = "127.0.0.1:2080"
|
||||
eCfg.DefaultReqType = utils.RATED
|
||||
eCfg.DefaultTOR = "call"
|
||||
eCfg.DefaultTenant = "cgrates.org"
|
||||
eCfg.DefaultSubject = "cgrates"
|
||||
eCfg.RoundingMethod = utils.ROUNDING_MIDDLE
|
||||
eCfg.RoundingDecimals = 4
|
||||
eCfg.XmlCfgDocument = nil
|
||||
eCfg.RaterEnabled = false
|
||||
eCfg.RaterBalancer = DISABLED
|
||||
eCfg.RaterListen = "127.0.0.1:2012"
|
||||
eCfg.RaterBalancer = ""
|
||||
eCfg.BalancerEnabled = false
|
||||
eCfg.BalancerListen = "127.0.0.1:2013"
|
||||
eCfg.SchedulerEnabled = false
|
||||
eCfg.CDRSEnabled = false
|
||||
eCfg.CDRSListen = "127.0.0.1:2022"
|
||||
eCfg.CDRSExtraFields = []string{}
|
||||
eCfg.CDRSExtraFields = []*utils.RSRField{}
|
||||
eCfg.CDRSMediator = ""
|
||||
eCfg.CdreCdrFormat = "csv"
|
||||
eCfg.CdreExtraFields = []string{}
|
||||
eCfg.CdreDir = "/var/log/cgrates/cdr/cdrexport/csv"
|
||||
eCfg.CdrcEnabled = false
|
||||
eCfg.CdrcCdrs = "127.0.0.1:2022"
|
||||
eCfg.CdrcCdrs = utils.INTERNAL
|
||||
eCfg.CdrcCdrsMethod = "http_cgr"
|
||||
eCfg.CdrcRunDelay = time.Duration(0)
|
||||
eCfg.CdrcCdrType = "csv"
|
||||
@@ -101,12 +99,12 @@ func TestDefaults(t *testing.T) {
|
||||
eCfg.CdrcAccountField = "5"
|
||||
eCfg.CdrcSubjectField = "6"
|
||||
eCfg.CdrcDestinationField = "7"
|
||||
eCfg.CdrcAnswerTimeField = "8"
|
||||
eCfg.CdrcDurationField = "9"
|
||||
eCfg.CdrcSetupTimeField = "8"
|
||||
eCfg.CdrcAnswerTimeField = "9"
|
||||
eCfg.CdrcDurationField = "10"
|
||||
eCfg.CdrcExtraFields = []string{}
|
||||
eCfg.MediatorEnabled = false
|
||||
eCfg.MediatorListen = "127.0.0.1:2032"
|
||||
eCfg.MediatorRater = "127.0.0.1:2012"
|
||||
eCfg.MediatorRater = "internal"
|
||||
eCfg.MediatorRaterReconnects = 3
|
||||
eCfg.MediatorRunIds = []string{}
|
||||
eCfg.MediatorSubjectFields = []string{}
|
||||
@@ -116,23 +114,55 @@ func TestDefaults(t *testing.T) {
|
||||
eCfg.MediatorTORFields = []string{}
|
||||
eCfg.MediatorAccountFields = []string{}
|
||||
eCfg.MediatorDestFields = []string{}
|
||||
eCfg.MediatorSetupTimeFields = []string{}
|
||||
eCfg.MediatorAnswerTimeFields = []string{}
|
||||
eCfg.MediatorDurationFields = []string{}
|
||||
eCfg.SMEnabled = false
|
||||
eCfg.SMSwitchType = FS
|
||||
eCfg.SMRater = "127.0.0.1:2012"
|
||||
eCfg.SMRater = "internal"
|
||||
eCfg.SMRaterReconnects = 3
|
||||
eCfg.SMDebitInterval = 10
|
||||
eCfg.SMMaxCallDuration = time.Duration(3) * time.Hour
|
||||
eCfg.SMRunIds = []string{}
|
||||
eCfg.SMReqTypeFields = []string{}
|
||||
eCfg.SMDirectionFields = []string{}
|
||||
eCfg.SMTenantFields = []string{}
|
||||
eCfg.SMTORFields = []string{}
|
||||
eCfg.SMAccountFields = []string{}
|
||||
eCfg.SMSubjectFields = []string{}
|
||||
eCfg.SMDestFields = []string{}
|
||||
eCfg.SMSetupTimeFields = []string{}
|
||||
eCfg.SMAnswerTimeFields = []string{}
|
||||
eCfg.SMDurationFields = []string{}
|
||||
eCfg.FreeswitchServer = "127.0.0.1:8021"
|
||||
eCfg.FreeswitchPass = "ClueCon"
|
||||
eCfg.FreeswitchReconnects = 5
|
||||
eCfg.HistoryAgentEnabled = false
|
||||
eCfg.HistoryServer = "127.0.0.1:2013"
|
||||
eCfg.HistoryServer = "internal"
|
||||
eCfg.HistoryServerEnabled = false
|
||||
eCfg.HistoryListen = "127.0.0.1:2013"
|
||||
eCfg.HistoryDir = "/var/log/cgrates/history"
|
||||
eCfg.HistorySaveInterval = time.Duration(1)*time.Second
|
||||
eCfg.HistorySaveInterval = time.Duration(1) * time.Second
|
||||
eCfg.MailerServer = "localhost:25"
|
||||
eCfg.MailerAuthUser = "cgrates"
|
||||
eCfg.MailerAuthPass = "CGRateS.org"
|
||||
eCfg.MailerFromAddr = "cgr-mailer@localhost.localdomain"
|
||||
eCfg.CdreExportedFields = []*utils.RSRField{
|
||||
&utils.RSRField{Id: utils.CGRID},
|
||||
&utils.RSRField{Id: utils.MEDI_RUNID},
|
||||
&utils.RSRField{Id: utils.ACCID},
|
||||
&utils.RSRField{Id: utils.CDRHOST},
|
||||
&utils.RSRField{Id: utils.REQTYPE},
|
||||
&utils.RSRField{Id: utils.DIRECTION},
|
||||
&utils.RSRField{Id: utils.TENANT},
|
||||
&utils.RSRField{Id: utils.TOR},
|
||||
&utils.RSRField{Id: utils.ACCOUNT},
|
||||
&utils.RSRField{Id: utils.SUBJECT},
|
||||
&utils.RSRField{Id: utils.DESTINATION},
|
||||
&utils.RSRField{Id: utils.SETUP_TIME},
|
||||
&utils.RSRField{Id: utils.ANSWER_TIME},
|
||||
&utils.RSRField{Id: utils.DURATION},
|
||||
&utils.RSRField{Id: utils.COST},
|
||||
}
|
||||
if !reflect.DeepEqual(cfg, eCfg) {
|
||||
t.Log(eCfg)
|
||||
t.Log(cfg)
|
||||
@@ -140,22 +170,23 @@ func TestDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure defaults did not change
|
||||
func TestDefaultsSanity(t *testing.T) {
|
||||
func TestSanityCheck(t *testing.T) {
|
||||
cfg := &CGRConfig{}
|
||||
errSet := cfg.setDefaults()
|
||||
if errSet != nil {
|
||||
t.Log(fmt.Sprintf("Coud not set defaults: %s!", errSet.Error()))
|
||||
t.FailNow()
|
||||
t.Error("Coud not set defaults: ", errSet.Error())
|
||||
}
|
||||
if (cfg.RaterListen != INTERNAL &&
|
||||
(cfg.RaterListen == cfg.BalancerListen ||
|
||||
cfg.RaterListen == cfg.CDRSListen ||
|
||||
cfg.RaterListen == cfg.MediatorListen)) ||
|
||||
(cfg.BalancerListen != INTERNAL && (cfg.BalancerListen == cfg.CDRSListen ||
|
||||
cfg.BalancerListen == cfg.MediatorListen)) ||
|
||||
(cfg.CDRSListen != INTERNAL && cfg.CDRSListen == cfg.MediatorListen) {
|
||||
t.Error("Listen defaults on the same port!")
|
||||
if err := cfg.checkConfigSanity(); err != nil {
|
||||
t.Error("Invalid defaults: ", err)
|
||||
}
|
||||
cfg.SMSubjectFields = []string{"sample1", "sample2", "sample3"}
|
||||
if err := cfg.checkConfigSanity(); err == nil {
|
||||
t.Error("Failed to detect config insanity")
|
||||
}
|
||||
cfg = &CGRConfig{}
|
||||
cfg.CdreCdrFormat = utils.CDRE_FIXED_WIDTH
|
||||
if err := cfg.checkConfigSanity(); err == nil {
|
||||
t.Error("Failed to detect fixed_width dependency on xml configuration")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +219,9 @@ func TestConfigFromFile(t *testing.T) {
|
||||
eCfg.StorDBUser = "test"
|
||||
eCfg.StorDBPass = "test"
|
||||
eCfg.DBDataEncoding = "test"
|
||||
eCfg.RPCEncoding = "test"
|
||||
eCfg.RPCJSONListen = "test"
|
||||
eCfg.RPCGOBListen = "test"
|
||||
eCfg.HTTPListen = "test"
|
||||
eCfg.DefaultReqType = "test"
|
||||
eCfg.DefaultTOR = "test"
|
||||
eCfg.DefaultTenant = "test"
|
||||
@@ -197,21 +230,18 @@ func TestConfigFromFile(t *testing.T) {
|
||||
eCfg.RoundingDecimals = 99
|
||||
eCfg.RaterEnabled = true
|
||||
eCfg.RaterBalancer = "test"
|
||||
eCfg.RaterListen = "test"
|
||||
eCfg.BalancerEnabled = true
|
||||
eCfg.BalancerListen = "test"
|
||||
eCfg.SchedulerEnabled = true
|
||||
eCfg.CDRSEnabled = true
|
||||
eCfg.CDRSListen = "test"
|
||||
eCfg.CDRSExtraFields = []string{"test"}
|
||||
eCfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "test"}}
|
||||
eCfg.CDRSMediator = "test"
|
||||
eCfg.CdreCdrFormat = "test"
|
||||
eCfg.CdreExtraFields = []string{"test"}
|
||||
eCfg.CdreExportedFields = []*utils.RSRField{&utils.RSRField{Id: "test"}}
|
||||
eCfg.CdreDir = "test"
|
||||
eCfg.CdrcEnabled = true
|
||||
eCfg.CdrcCdrs = "test"
|
||||
eCfg.CdrcCdrsMethod = "test"
|
||||
eCfg.CdrcRunDelay = time.Duration(99)*time.Second
|
||||
eCfg.CdrcRunDelay = time.Duration(99) * time.Second
|
||||
eCfg.CdrcCdrType = "test"
|
||||
eCfg.CdrcCdrInDir = "test"
|
||||
eCfg.CdrcCdrOutDir = "test"
|
||||
@@ -224,11 +254,11 @@ func TestConfigFromFile(t *testing.T) {
|
||||
eCfg.CdrcAccountField = "test"
|
||||
eCfg.CdrcSubjectField = "test"
|
||||
eCfg.CdrcDestinationField = "test"
|
||||
eCfg.CdrcSetupTimeField = "test"
|
||||
eCfg.CdrcAnswerTimeField = "test"
|
||||
eCfg.CdrcDurationField = "test"
|
||||
eCfg.CdrcExtraFields = []string{"test"}
|
||||
eCfg.MediatorEnabled = true
|
||||
eCfg.MediatorListen = "test"
|
||||
eCfg.MediatorRater = "test"
|
||||
eCfg.MediatorRaterReconnects = 99
|
||||
eCfg.MediatorRunIds = []string{"test"}
|
||||
@@ -239,6 +269,7 @@ func TestConfigFromFile(t *testing.T) {
|
||||
eCfg.MediatorTORFields = []string{"test"}
|
||||
eCfg.MediatorAccountFields = []string{"test"}
|
||||
eCfg.MediatorDestFields = []string{"test"}
|
||||
eCfg.MediatorSetupTimeFields = []string{"test"}
|
||||
eCfg.MediatorAnswerTimeFields = []string{"test"}
|
||||
eCfg.MediatorDurationFields = []string{"test"}
|
||||
eCfg.SMEnabled = true
|
||||
@@ -246,16 +277,30 @@ func TestConfigFromFile(t *testing.T) {
|
||||
eCfg.SMRater = "test"
|
||||
eCfg.SMRaterReconnects = 99
|
||||
eCfg.SMDebitInterval = 99
|
||||
eCfg.SMMaxCallDuration = time.Duration(99)*time.Second
|
||||
eCfg.SMMaxCallDuration = time.Duration(99) * time.Second
|
||||
eCfg.SMRunIds = []string{"test"}
|
||||
eCfg.SMReqTypeFields = []string{"test"}
|
||||
eCfg.SMDirectionFields = []string{"test"}
|
||||
eCfg.SMTenantFields = []string{"test"}
|
||||
eCfg.SMTORFields = []string{"test"}
|
||||
eCfg.SMAccountFields = []string{"test"}
|
||||
eCfg.SMSubjectFields = []string{"test"}
|
||||
eCfg.SMDestFields = []string{"test"}
|
||||
eCfg.SMSetupTimeFields = []string{"test"}
|
||||
eCfg.SMAnswerTimeFields = []string{"test"}
|
||||
eCfg.SMDurationFields = []string{"test"}
|
||||
eCfg.FreeswitchServer = "test"
|
||||
eCfg.FreeswitchPass = "test"
|
||||
eCfg.FreeswitchReconnects = 99
|
||||
eCfg.HistoryAgentEnabled = true
|
||||
eCfg.HistoryServer = "test"
|
||||
eCfg.HistoryServerEnabled = true
|
||||
eCfg.HistoryListen = "test"
|
||||
eCfg.HistoryDir = "test"
|
||||
eCfg.HistorySaveInterval = time.Duration(99)*time.Second
|
||||
eCfg.HistorySaveInterval = time.Duration(99) * time.Second
|
||||
eCfg.MailerServer = "test"
|
||||
eCfg.MailerAuthUser = "test"
|
||||
eCfg.MailerAuthPass = "test"
|
||||
eCfg.MailerFromAddr = "test"
|
||||
if !reflect.DeepEqual(cfg, eCfg) {
|
||||
t.Log(eCfg)
|
||||
t.Log(cfg)
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"code.google.com/p/goconf/conf"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
// Adds support for slice values in config
|
||||
@@ -42,3 +44,23 @@ func ConfigSlice(c *conf.ConfigFile, section, valName string) ([]string, error)
|
||||
}
|
||||
return cfgValStrs, nil
|
||||
}
|
||||
|
||||
func ParseRSRFields(configVal string) ([]*utils.RSRField, error) {
|
||||
cfgValStrs := strings.Split(configVal, string(utils.CSV_SEP))
|
||||
if len(cfgValStrs) == 1 && cfgValStrs[0] == "" { // Prevents returning iterable with empty value
|
||||
return []*utils.RSRField{}, nil
|
||||
}
|
||||
rsrFields := make([]*utils.RSRField, len(cfgValStrs))
|
||||
for idx, cfgValStr := range cfgValStrs {
|
||||
if len(cfgValStr) == 0 { //One empty element is presented when splitting empty string
|
||||
return nil, errors.New("Empty values in config slice")
|
||||
|
||||
}
|
||||
if rsrField, err := utils.NewRSRField(cfgValStr); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
rsrFields[idx] = rsrField
|
||||
}
|
||||
}
|
||||
return rsrFields, nil
|
||||
}
|
||||
|
||||
39
config/helpers_test.go
Normal file
39
config/helpers_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func TestParseRSRFields(t *testing.T) {
|
||||
fields := `host,~sip_redirected_to:s/sip:\+49(\d+)@/0$1/,destination`
|
||||
expectParsedFields := []*utils.RSRField{&utils.RSRField{Id: "host"},
|
||||
&utils.RSRField{Id: "sip_redirected_to", RSRule: &utils.ReSearchReplace{regexp.MustCompile(`sip:\+49(\d+)@`), "0$1"}},
|
||||
&utils.RSRField{Id: "destination"}}
|
||||
if parsedFields, err := ParseRSRFields(fields); err != nil {
|
||||
t.Error("Unexpected error: ", err.Error())
|
||||
} else if !reflect.DeepEqual(parsedFields, expectParsedFields) {
|
||||
t.Errorf("Unexpected value of parsed fields")
|
||||
}
|
||||
}
|
||||
@@ -2,56 +2,55 @@
|
||||
#
|
||||
|
||||
[global]
|
||||
ratingdb_type = test # Rating subsystem database: <redis>.
|
||||
ratingdb_host = test # Rating subsystem database host address.
|
||||
ratingdb_port = test # Rating subsystem port to reach the database.
|
||||
ratingdb_name = test # Rating subsystem database name to connect to.
|
||||
ratingdb_user = test # Rating subsystem username to use when connecting to database.
|
||||
ratingdb_passwd = test # Rating subsystem password to use when connecting to database.
|
||||
accountdb_type = test # Accounting subsystem database: <redis>.
|
||||
accountdb_host = test # Accounting subsystem database host address.
|
||||
accountdb_port = test # Accounting subsystem port to reach the database.
|
||||
accountdb_name = test # Accounting subsystem database name to connect to.
|
||||
accountdb_user = test # Accounting subsystem username to use when connecting to database.
|
||||
accountdb_passwd = test # Accounting subsystem password to use when connecting to database.
|
||||
stordb_type = test # Log/stored database type to use: <same|postgres|mongo|redis>
|
||||
stordb_host = test # The host to connect to. Values that start with / are for UNIX domain sockets.
|
||||
stordb_port = test # The port to reach the logdb.
|
||||
stordb_name = test # The name of the log database to connect to.
|
||||
stordb_user = test # Username to use when connecting to logdb.
|
||||
stordb_passwd = test # Password to use when connecting to logdb.
|
||||
dbdata_encoding = test # The encoding used to store object data in strings: <msgpack|json>
|
||||
rpc_encoding = test # RPC encoding used on APIs: <gob|json>.
|
||||
default_reqtype = test # Default request type to consider when missing from requests: <""|prepaid|postpaid|pseudoprepaid|rated>.
|
||||
default_tor = test # Default Type of Record to consider when missing from requests.
|
||||
default_tenant = test # Default Tenant to consider when missing from requests.
|
||||
default_subject = test # Default rating Subject to consider when missing from requests.
|
||||
rounding_method = test # Rounding method for floats/costs: <up|middle|down>
|
||||
rounding_decimals = 99 # Number of decimals to round floats/costs at
|
||||
ratingdb_type = test # Rating subsystem database: <redis>.
|
||||
ratingdb_host = test # Rating subsystem database host address.
|
||||
ratingdb_port = test # Rating subsystem port to reach the database.
|
||||
ratingdb_name = test # Rating subsystem database name to connect to.
|
||||
ratingdb_user = test # Rating subsystem username to use when connecting to database.
|
||||
ratingdb_passwd = test # Rating subsystem password to use when connecting to database.
|
||||
accountdb_type = test # Accounting subsystem database: <redis>.
|
||||
accountdb_host = test # Accounting subsystem database host address.
|
||||
accountdb_port = test # Accounting subsystem port to reach the database.
|
||||
accountdb_name = test # Accounting subsystem database name to connect to.
|
||||
accountdb_user = test # Accounting subsystem username to use when connecting to database.
|
||||
accountdb_passwd = test # Accounting subsystem password to use when connecting to database.
|
||||
stordb_type = test # Log/stored database type to use: <same|postgres|mongo|redis>
|
||||
stordb_host = test # The host to connect to. Values that start with / are for UNIX domain sockets.
|
||||
stordb_port = test # The port to reach the logdb.
|
||||
stordb_name = test # The name of the log database to connect to.
|
||||
stordb_user = test # Username to use when connecting to logdb.
|
||||
stordb_passwd = test # Password to use when connecting to logdb.
|
||||
dbdata_encoding = test # The encoding used to store object data in strings: <msgpack|json>
|
||||
rpc_json_listen = test # RPC JSON listening address
|
||||
rpc_gob_listen = test # RPC GOB listening address
|
||||
http_listen = test # HTTP listening address
|
||||
default_reqtype = test # Default request type to consider when missing from requests: <""|prepaid|postpaid|pseudoprepaid|rated>.
|
||||
default_tor = test # Default Type of Record to consider when missing from requests.
|
||||
default_tenant = test # Default Tenant to consider when missing from requests.
|
||||
default_subject = test # Default rating Subject to consider when missing from requests.
|
||||
rounding_method = test # Rounding method for floats/costs: <up|middle|down>
|
||||
rounding_decimals = 99 # Number of decimals to round floats/costs at
|
||||
|
||||
|
||||
[balancer]
|
||||
enabled = true # Start Balancer service: <true|false>.
|
||||
listen = test # Balancer listen interface: <disabled|x.y.z.y:1234>.
|
||||
|
||||
[rater]
|
||||
enabled = true # Enable Rater service: <true|false>.
|
||||
balancer = test # Register to Balancer as worker: <enabled|disabled>.
|
||||
listen = test # Rater's listening interface: <internal|x.y.z.y:1234>.
|
||||
balancer = test # Register to Balancer as worker: <enabled|disabled>.
|
||||
|
||||
[scheduler]
|
||||
enabled = true # Starts Scheduler service: <true|false>.
|
||||
|
||||
[cdrs]
|
||||
enabled = true # Start the CDR Server service: <true|false>.
|
||||
listen=test # CDRS's listening interface: <x.y.z.y:1234>.
|
||||
extra_fields = test # Extra fields to store in CDRs
|
||||
mediator = test # Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
|
||||
|
||||
[cdre]
|
||||
cdr_format = test # Exported CDRs format <csv>
|
||||
extra_fields = test # List of extra fields to be exported out in CDRs
|
||||
export_dir = test # Path where the exported CDRs will be placed
|
||||
cdr_format = test # Exported CDRs format <csv>
|
||||
export_dir = test # Path where the exported CDRs will be placed
|
||||
export_template = test # List of fields in the exported CDRs
|
||||
|
||||
[cdrc]
|
||||
enabled = true # Enable CDR client functionality
|
||||
@@ -70,46 +69,62 @@ tor_field = test # Type of Record field identifier. Use index numbers in case
|
||||
account_field = test # Account field identifier. Use index numbers in case of .csv cdrs.
|
||||
subject_field = test # Subject field identifier. Use index numbers in case of .csv CDRs.
|
||||
destination_field = test # Destination field identifier. Use index numbers in case of .csv cdrs.
|
||||
setup_time_field = test # Answer time field identifier. Use index numbers in case of .csv cdrs.
|
||||
answer_time_field = test # Answer time field identifier. Use index numbers in case of .csv cdrs.
|
||||
duration_field = test # Duration field identifier. Use index numbers in case of .csv cdrs.
|
||||
extra_fields = test # Field identifiers of the fields to add in extra fields section, special format in case of .csv "index1:field1,index2:field2"
|
||||
|
||||
[mediator]
|
||||
enabled = true # Starts Mediator service: <true|false>.
|
||||
listen=test # Mediator's listening interface: <internal>.
|
||||
rater = test # Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
rater_reconnects = 99 # Number of reconnects to rater before giving up.
|
||||
rater = test # Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
rater_reconnects = 99 # Number of reconnects to rater before giving up.
|
||||
run_ids = test # Identifiers for each mediation run on CDRs
|
||||
subject_fields = test # Name of subject fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
reqtype_fields = test # Name of request type fields to be used during mediation. Use index number in case of .csv cdrs.
|
||||
reqtype_fields = test # Name of request type fields to be used during mediation. Use index number in case of .csv cdrs.
|
||||
direction_fields = test # Name of direction fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
tenant_fields = test # Name of tenant fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
tor_fields = test # Name of tor fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
tor_fields = test # Name of tor fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
account_fields = test # Name of account fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
destination_fields = test # Name of destination fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
answer_time_fields = test # Name of time_answer fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
setup_time_fields = test # Name of setup_time fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
answer_time_fields = test # Name of answer_time fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
duration_fields = test # Name of duration fields to be used during mediation. Use index numbers in case of .csv cdrs.
|
||||
|
||||
[session_manager]
|
||||
enabled = true # Starts SessionManager service: <true|false>.
|
||||
switch_type = test # Defines the type of switch behind: <freeswitch>.
|
||||
rater = test # Address where to reach the Rater.
|
||||
rater = test # Address where to reach the Rater.
|
||||
rater_reconnects = 99 # Number of reconnects to rater before giving up.
|
||||
debit_interval = 99 # Interval to perform debits on.
|
||||
max_call_duration = 99 # Maximum call duration a prepaid call can last
|
||||
run_ids = test # Identifiers of additional sessions control.
|
||||
reqtype_fields = test # Name of request type fields to be used during additional sessions control <""|*default|field_name>.
|
||||
direction_fields = test # Name of direction fields to be used during additional sessions control <""|*default|field_name>.
|
||||
tenant_fields = test # Name of tenant fields to be used during additional sessions control <""|*default|field_name>.
|
||||
tor_fields = test # Name of tor fields to be used during additional sessions control <""|*default|field_name>.
|
||||
account_fields = test # Name of account fields to be used during additional sessions control <""|*default|field_name>.
|
||||
subject_fields = test # Name of fields to be used during additional sessions control <""|*default|field_name>.
|
||||
destination_fields = test # Name of destination fields to be used during additional sessions control <""|*default|field_name>.
|
||||
setup_time_fields = test # Name of setup_time fields to be used during additional sessions control <""|*default|field_name>.
|
||||
answer_time_fields = test # Name of answer_time fields to be used during additional sessions control <""|*default|field_name>.
|
||||
duration_fields = test # Name of duration fields to be used during additional sessions control <""|*default|field_name>.
|
||||
|
||||
[freeswitch]
|
||||
server = test # Adress where to connect to FreeSWITCH socket.
|
||||
server = test # Adress where to connect to FreeSWITCH socket.
|
||||
passwd = test # FreeSWITCH socket password.
|
||||
reconnects = 99 # Number of attempts on connect failure.
|
||||
|
||||
[history_server]
|
||||
enabled = true # Starts History service: <true|false>.
|
||||
listen = test # Listening addres for history server: <internal|x.y.z.y:1234>
|
||||
history_dir = test # Location on disk where to store history files.
|
||||
save_interval = 99 # Timeout duration between saves
|
||||
enabled = true # Starts History service: <true|false>.
|
||||
history_dir = test # Location on disk where to store history files.
|
||||
save_interval = 99 # Timeout duration between saves
|
||||
|
||||
[history_agent]
|
||||
enabled = true # Starts History as a client: <true|false>.
|
||||
server = test # Address where to reach the master history server: <internal|x.y.z.y:1234>
|
||||
enabled = true # Starts History as a client: <true|false>.
|
||||
server = test # Address where to reach the master history server: <internal|x.y.z.y:1234>
|
||||
|
||||
[mailer]
|
||||
server = test # The server to use when sending emails out
|
||||
auth_user = test # Authenticate to email server using this user
|
||||
auth_passwd = test # Authenticate to email server with this password
|
||||
from_address = test # From address used when sending emails out
|
||||
|
||||
123
config/xmlconfig.go
Normal file
123
config/xmlconfig.go
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Decodes a reader enforcing specific format of the configuration file
|
||||
func ParseCgrXmlConfig(reader io.Reader) (*CgrXmlCfgDocument, error) {
|
||||
xmlConfig := new(CgrXmlCfgDocument)
|
||||
decoder := xml.NewDecoder(reader)
|
||||
if err := decoder.Decode(xmlConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return xmlConfig, nil
|
||||
}
|
||||
|
||||
// Define a format for configuration file, one doc contains more configuration instances, identified by section, type and id
|
||||
type CgrXmlCfgDocument struct {
|
||||
XMLName xml.Name `xml:"document"`
|
||||
Type string `xml:"type,attr"`
|
||||
Configurations []*CgrXmlConfiguration `xml:"configuration"`
|
||||
cdrefws map[string]*CgrXmlCdreFwCfg // Cache for processed fixed width config instances, key will be the id of the instance
|
||||
}
|
||||
|
||||
// Storage for raw configuration
|
||||
type CgrXmlConfiguration struct {
|
||||
XMLName xml.Name `xml:"configuration"`
|
||||
Section string `xml:"section,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Id string `xml:"id,attr"`
|
||||
RawConfig []byte `xml:",innerxml"` // Used to store the configuration struct, as raw so we can store different types
|
||||
}
|
||||
|
||||
// The CdrExporter Fixed Width configuration instance
|
||||
type CgrXmlCdreFwCfg struct {
|
||||
Header *CgrXmlCfgCdrHeader `xml:"header"`
|
||||
Content *CgrXmlCfgCdrContent `xml:"content"`
|
||||
Trailer *CgrXmlCfgCdrTrailer `xml:"trailer"`
|
||||
}
|
||||
|
||||
// CDR header
|
||||
type CgrXmlCfgCdrHeader struct {
|
||||
XMLName xml.Name `xml:"header"`
|
||||
Fields []*CgrXmlCfgCdrField `xml:"fields>field"`
|
||||
}
|
||||
|
||||
// CDR content
|
||||
type CgrXmlCfgCdrContent struct {
|
||||
XMLName xml.Name `xml:"content"`
|
||||
Fields []*CgrXmlCfgCdrField `xml:"fields>field"`
|
||||
}
|
||||
|
||||
// CDR trailer
|
||||
type CgrXmlCfgCdrTrailer struct {
|
||||
XMLName xml.Name `xml:"trailer"`
|
||||
Fields []*CgrXmlCfgCdrField `xml:"fields>field"`
|
||||
}
|
||||
|
||||
// CDR field
|
||||
type CgrXmlCfgCdrField struct {
|
||||
XMLName xml.Name `xml:"field"`
|
||||
Name string `xml:"name,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Value string `xml:"value,attr"`
|
||||
Width int `xml:"width,attr"` // Field width
|
||||
Strip string `xml:"strip,attr"` // Strip strategy in case value is bigger than field width <""|left|xleft|right|xright>
|
||||
Padding string `xml:"padding,attr"` // Padding strategy in case of value is smaller than width <""left|zeroleft|right>
|
||||
Layout string `xml:"layout,attr"` // Eg. time format layout
|
||||
Mandatory bool `xml:"mandatory,attr"` // If field is mandatory, empty value will be considered as error and CDR will not be exported
|
||||
}
|
||||
|
||||
// Avoid building from raw config string always, so build cache here
|
||||
func (xmlCfg *CgrXmlCfgDocument) cacheCdreFWCfgs() error {
|
||||
xmlCfg.cdrefws = make(map[string]*CgrXmlCdreFwCfg)
|
||||
for _, cfgInst := range xmlCfg.Configurations {
|
||||
if cfgInst.Section == utils.CDRE || cfgInst.Type == utils.CDRE_FIXED_WIDTH {
|
||||
cdrefwCfg := new(CgrXmlCdreFwCfg)
|
||||
rawConfig := append([]byte("<element>"), cfgInst.RawConfig...) // Encapsulate the rawConfig in one element so we can Unmarshall into one struct
|
||||
rawConfig = append(rawConfig, []byte("</element>")...)
|
||||
if err := xml.Unmarshal(rawConfig, cdrefwCfg); err != nil {
|
||||
return err
|
||||
} else if cdrefwCfg == nil {
|
||||
return fmt.Errorf("Could not unmarshal CgrXmlCdreFwCfg: %s", cfgInst.Id)
|
||||
} else { // All good, cache the config instance
|
||||
xmlCfg.cdrefws[cfgInst.Id] = cdrefwCfg
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (xmlCfg *CgrXmlCfgDocument) GetCdreFWCfg(instName string) (*CgrXmlCdreFwCfg, error) {
|
||||
if len(xmlCfg.cdrefws) == 0 { // First time, cache also
|
||||
if err := xmlCfg.cacheCdreFWCfgs(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cfg, hasIt := xmlCfg.cdrefws[instName]; hasIt {
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
119
config/xmlconfig_test.go
Normal file
119
config/xmlconfig_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var cfgDoc *CgrXmlCfgDocument // Will be populated by first test
|
||||
|
||||
func TestParseXmlConfig(t *testing.T) {
|
||||
cfgXmlStr := `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<document type="cgrates/xml">
|
||||
<configuration section="cdre" type="fixed_width" id="CDRE-FW1">
|
||||
<header>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="10" width="2"/>
|
||||
<field name="Filler1" type="filler" width="3"/>
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3"/>
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5"/>
|
||||
<field name="LastCdr" type="metatag" value="last_cdr_time" layout="020106150400" width="12"/>
|
||||
<field name="FileCreationfTime" type="metatag" value="time_now" layout="020106150400" width="12"/>
|
||||
<field name="Version" type="constant" value="01" width="2"/>
|
||||
<field name="Filler2" type="filler" width="105"/>
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="20" width="2"/>
|
||||
<field name="Account" type="cdrfield" value="cgrid" width="12" mandatory="true"/>
|
||||
<field name="Subject" type="cdrfield" value="subject" strip="left" padding="left" width="5"/>
|
||||
<field name="CLI" type="cdrfield" value="cli" strip="xright" width="15"/>
|
||||
<field name="Destination" type="cdrfield" value="destination" strip="xright" width="24"/>
|
||||
<field name="TOR" type="constant" value="02" width="2"/>
|
||||
<field name="SubtypeTOR" type="constant" value="11" width="4"/>
|
||||
<field name="SetupTime" type="cdrfield" value="start_time" layout="020106150400" width="12"/>
|
||||
<field name="Duration" type="cdrfield" value="duration" width="6"/>
|
||||
<field name="DataVolume" type="filler" width="6"/>
|
||||
<field name="TaxCode" type="constant" value="1" width="1"/>
|
||||
<field name="OperatorCode" type="cdrfield" value="operator" width="2"/>
|
||||
<field name="ProductId" type="cdrfield" value="productid" width="5"/>
|
||||
<field name="NetworkId" type="constant" value="3" width="1"/>
|
||||
<field name="CallId" type="cdrfield" value="accid" width="16"/>
|
||||
<field name="Filler" type="filler" width="8"/>
|
||||
<field name="Filler" type="filler" width="8"/>
|
||||
<field name="TerminationCode" type="concatenated_cdrfield" value="operator,product" width="5"/>
|
||||
<field name="Cost" type="cdrfield" value="cost" padding="zeroleft" width="9"/>
|
||||
<field name="CalledMask" type="cdrfield" value="calledmask" width="1"/>
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="90" width="2"/>
|
||||
<field name="Filler1" type="filler" width="3"/>
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3"/>
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5"/>
|
||||
<field name="NumberOfRecords" type="metatag" value="cdrs_number" padding="zeroleft" width="6"/>
|
||||
<field name="CdrsDuration" type="metatag" value="cdrs_duration" padding="zeroleft" width="8"/>
|
||||
<field name="FirstCdrTime" type="metatag" value="first_cdr_time" layout="020106150400" width="12"/>
|
||||
<field name="LastCdrTime" type="metatag" value="last_cdr_time" layout="020106150400" width="12"/>
|
||||
<field name="Filler1" type="filler" width="93"/>
|
||||
</fields>
|
||||
</trailer>
|
||||
</configuration>
|
||||
</document>`
|
||||
var err error
|
||||
reader := strings.NewReader(cfgXmlStr)
|
||||
if cfgDoc, err = ParseCgrXmlConfig(reader); err != nil {
|
||||
t.Error(err.Error())
|
||||
} else if cfgDoc == nil {
|
||||
t.Fatal("Could not parse xml configuration document")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheCdreFWCfgs(t *testing.T) {
|
||||
if len(cfgDoc.cdrefws) != 0 {
|
||||
t.Error("Cache should be empty before caching")
|
||||
}
|
||||
if err := cfgDoc.cacheCdreFWCfgs(); err != nil {
|
||||
t.Error(err)
|
||||
} else if len(cfgDoc.cdrefws) != 1 {
|
||||
t.Error("Did not cache")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCdreFWCfg(t *testing.T) {
|
||||
cdreFWCfg, err := cfgDoc.GetCdreFWCfg("CDRE-FW1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if cdreFWCfg == nil {
|
||||
t.Error("Could not parse CdreFw instance")
|
||||
}
|
||||
if len(cdreFWCfg.Header.Fields) != 8 {
|
||||
t.Error("Unexpected number of header fields parsed", len(cdreFWCfg.Header.Fields))
|
||||
}
|
||||
if len(cdreFWCfg.Content.Fields) != 20 {
|
||||
t.Error("Unexpected number of content fields parsed", len(cdreFWCfg.Content.Fields))
|
||||
}
|
||||
if len(cdreFWCfg.Trailer.Fields) != 9 {
|
||||
t.Error("Unexpected number of trailer fields parsed", len(cdreFWCfg.Trailer.Fields))
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,9 @@ package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/apier/v1"
|
||||
"strconv"
|
||||
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -36,7 +38,7 @@ type CmdAddAccount struct {
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdAddAccount) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] add_account <tenant> <account> <type=prepaid|postpaid> <actiontimingsid> [<direction>]")
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] add_account <tenant> <account> <allownegative> <actiontimingsid> [<direction>]")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
@@ -55,8 +57,12 @@ func (self *CmdAddAccount) FromArgs(args []string) error {
|
||||
self.defaults()
|
||||
self.rpcParams.Tenant = args[2]
|
||||
self.rpcParams.Account = args[3]
|
||||
self.rpcParams.Type = args[4]
|
||||
self.rpcParams.ActionTimingsId = args[5]
|
||||
if an, err := strconv.ParseBool(args[4]); err != nil {
|
||||
return fmt.Errorf("Error parsing allownegative boolean: ", args[4])
|
||||
} else {
|
||||
self.rpcParams.AllowNegative = an
|
||||
}
|
||||
self.rpcParams.ActionPlanId = args[5]
|
||||
if len(args) > 6 {
|
||||
self.rpcParams.Direction = args[6]
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/apier/v1"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"strconv"
|
||||
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -38,13 +39,13 @@ type CmdAddBalance struct {
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdAddBalance) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] add_balance <tenant> <account> <value> [<balanceid=monetary|sms|internet|internet_time|minutes> [<weight> [overwrite]]]")
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] add_balance <tenant> <account> <value> [*monetary|*sms|*internet|*internet_time|*minutes [<destinationid> [<weight> [overwrite]]]]")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdAddBalance) defaults() error {
|
||||
self.rpcMethod = "ApierV1.AddBalance"
|
||||
self.rpcParams = &apier.AttrAddBalance{BalanceId: engine.CREDIT}
|
||||
self.rpcParams = &apier.AttrAddBalance{BalanceType: engine.CREDIT}
|
||||
self.rpcParams.Direction = "*out"
|
||||
return nil
|
||||
}
|
||||
@@ -65,15 +66,18 @@ func (self *CmdAddBalance) FromArgs(args []string) error {
|
||||
}
|
||||
self.rpcParams.Value = value
|
||||
if len(args) > 5 {
|
||||
self.rpcParams.BalanceId = args[5]
|
||||
self.rpcParams.BalanceType = args[5]
|
||||
}
|
||||
if len(args) > 6 {
|
||||
if self.rpcParams.Weight, err = strconv.ParseFloat(args[6], 64); err != nil {
|
||||
self.rpcParams.DestinationId = args[6]
|
||||
}
|
||||
if len(args) > 7 {
|
||||
if self.rpcParams.Weight, err = strconv.ParseFloat(args[7], 64); err != nil {
|
||||
return fmt.Errorf("Cannot parse weight parameter")
|
||||
}
|
||||
}
|
||||
if len(args) > 7 {
|
||||
if args[7] == "overwrite" {
|
||||
if len(args) > 8 {
|
||||
if args[8] == "overwrite" {
|
||||
self.rpcParams.Overwrite = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/apier/v1"
|
||||
"strconv"
|
||||
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -56,7 +57,7 @@ func (self *CmdAddTriggeredAction) FromArgs(args []string) error {
|
||||
self.defaults()
|
||||
self.rpcParams.Tenant = args[2]
|
||||
self.rpcParams.Account = args[3]
|
||||
self.rpcParams.BalanceId = args[4]
|
||||
self.rpcParams.BalanceType = args[4]
|
||||
thresholdvalue, err := strconv.ParseFloat(args[5], 64)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
97
console/debit_balance.go
Normal file
97
console/debit_balance.go
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["debit_balance"] = &CmdDebitBalance{}
|
||||
}
|
||||
|
||||
// Commander implementation
|
||||
type CmdDebitBalance struct {
|
||||
rpcMethod string
|
||||
rpcParams *engine.CallDescriptor
|
||||
rpcResult engine.CallCost
|
||||
}
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdDebitBalance) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] debit_balance <tor> <tenant> <subject> <destination> <start_time|*now> <duration>")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdDebitBalance) defaults() error {
|
||||
self.rpcMethod = "Responder.Debit"
|
||||
self.rpcParams = &engine.CallDescriptor{Direction: "*out"}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses command line args and builds CmdBalance value
|
||||
func (self *CmdDebitBalance) FromArgs(args []string) error {
|
||||
if len(args) != 8 {
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
// Args look OK, set defaults before going further
|
||||
self.defaults()
|
||||
var tStart time.Time
|
||||
var err error
|
||||
if args[6] == "*now" {
|
||||
tStart = time.Now()
|
||||
} else {
|
||||
tStart, err = utils.ParseDate(args[6])
|
||||
if err != nil {
|
||||
fmt.Println("\n*start_time* should have one of the formats:")
|
||||
fmt.Println("\ttime.RFC3339\teg:2013-08-07T17:30:00Z in UTC")
|
||||
fmt.Println("\tunix time\teg: 1383823746")
|
||||
fmt.Println("\t*now\t\tmetafunction transformed into localtime at query time")
|
||||
fmt.Println("\t+dur\t\tduration to be added to localtime (valid suffixes: ns, us/µs, ms, s, m, h)\n")
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
}
|
||||
callDur, err := utils.ParseDurationWithSecs(args[7])
|
||||
if err != nil {
|
||||
fmt.Println("\n\tExample durations: 60s for 60 seconds, 25m for 25minutes, 1m25s for one minute and 25 seconds\n")
|
||||
}
|
||||
self.rpcParams.TOR = args[2]
|
||||
self.rpcParams.Tenant = args[3]
|
||||
self.rpcParams.Subject = args[4]
|
||||
self.rpcParams.Destination = args[5]
|
||||
self.rpcParams.TimeStart = tStart
|
||||
self.rpcParams.CallDuration = callDur
|
||||
self.rpcParams.TimeEnd = tStart.Add(callDur)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CmdDebitBalance) RpcMethod() string {
|
||||
return self.rpcMethod
|
||||
}
|
||||
|
||||
func (self *CmdDebitBalance) RpcParams() interface{} {
|
||||
return self.rpcParams
|
||||
}
|
||||
|
||||
func (self *CmdDebitBalance) RpcResult() interface{} {
|
||||
return &self.rpcResult
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/apier/v1"
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -37,13 +37,13 @@ type CmdExportCdrs struct {
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdExportCdrs) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] export_cdrs <dry_run|csv> [<start_time|*one_month> [<stop_time> [remove_from_db]]]")
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] export_cdrs <dry_run|csv> [<start_time|*one_month> [<stop_time> ]]")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdExportCdrs) defaults() error {
|
||||
self.rpcMethod = "ApierV1.ExportCdrsToFile"
|
||||
self.rpcParams = &utils.AttrExpFileCdrs{CdrFormat:"csv"}
|
||||
self.rpcParams = &utils.AttrExpFileCdrs{CdrFormat: "csv"}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -60,20 +60,17 @@ func (self *CmdExportCdrs) FromArgs(args []string) error {
|
||||
switch len(args) {
|
||||
case 4:
|
||||
timeStart = args[3]
|
||||
|
||||
|
||||
case 5:
|
||||
timeStart = args[3]
|
||||
timeEnd = args[4]
|
||||
case 6:
|
||||
timeStart = args[3]
|
||||
timeEnd = args[4]
|
||||
if args[5] == "remove_from_db" {
|
||||
self.rpcParams.RemoveFromDb = true
|
||||
}
|
||||
}
|
||||
if timeStart == "*one_month" {
|
||||
now := time.Now()
|
||||
self.rpcParams.TimeStart = now.AddDate(0,-1,0).String()
|
||||
self.rpcParams.TimeStart = now.AddDate(0, -1, 0).String()
|
||||
self.rpcParams.TimeEnd = now.String()
|
||||
} else {
|
||||
self.rpcParams.TimeStart = timeStart
|
||||
|
||||
@@ -20,36 +20,37 @@ package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/apier/v1"
|
||||
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["get_balance"] = &CmdGetBalance{}
|
||||
commands["get_account"] = &CmdGetAccount{}
|
||||
}
|
||||
|
||||
// Commander implementation
|
||||
type CmdGetBalance struct {
|
||||
type CmdGetAccount struct {
|
||||
rpcMethod string
|
||||
rpcParams *apier.AttrGetBalance
|
||||
rpcResult float64
|
||||
rpcParams *apier.AttrGetAccount
|
||||
rpcResult *engine.Account
|
||||
}
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdGetBalance) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] get_balance <tenant> <account> [<balanceid=monetary|sms|internet|internet_time|minutes> [<direction>]]")
|
||||
func (self *CmdGetAccount) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] get_account <tenant> <account>")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdGetBalance) defaults() error {
|
||||
self.rpcMethod = "ApierV1.GetBalance"
|
||||
self.rpcParams = &apier.AttrGetBalance{BalanceId: engine.CREDIT}
|
||||
func (self *CmdGetAccount) defaults() error {
|
||||
self.rpcMethod = "ApierV1.GetAccount"
|
||||
self.rpcParams = &apier.AttrGetAccount{BalanceType: engine.CREDIT}
|
||||
self.rpcParams.Direction = "*out"
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses command line args and builds CmdBalance value
|
||||
func (self *CmdGetBalance) FromArgs(args []string) error {
|
||||
func (self *CmdGetAccount) FromArgs(args []string) error {
|
||||
if len(args) < 4 {
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
@@ -57,24 +58,17 @@ func (self *CmdGetBalance) FromArgs(args []string) error {
|
||||
self.defaults()
|
||||
self.rpcParams.Tenant = args[2]
|
||||
self.rpcParams.Account = args[3]
|
||||
if len(args) > 4 {
|
||||
self.rpcParams.BalanceId = args[4]
|
||||
}
|
||||
if len(args) > 5 {
|
||||
|
||||
self.rpcParams.Direction = args[5]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CmdGetBalance) RpcMethod() string {
|
||||
func (self *CmdGetAccount) RpcMethod() string {
|
||||
return self.rpcMethod
|
||||
}
|
||||
|
||||
func (self *CmdGetBalance) RpcParams() interface{} {
|
||||
func (self *CmdGetAccount) RpcParams() interface{} {
|
||||
return self.rpcParams
|
||||
}
|
||||
|
||||
func (self *CmdGetBalance) RpcResult() interface{} {
|
||||
func (self *CmdGetAccount) RpcResult() interface{} {
|
||||
return &self.rpcResult
|
||||
}
|
||||
76
console/get_callcost.go
Normal file
76
console/get_callcost.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/apier"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["get_callcost"] = &CmdGetCallCost{}
|
||||
}
|
||||
|
||||
// Commander implementation
|
||||
type CmdGetCallCost struct {
|
||||
rpcMethod string
|
||||
rpcParams *apier.AttrGetCallCost
|
||||
rpcResult *engine.CallCost
|
||||
}
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdGetCallCost) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] get_callcost <cgrid> [<runid>]")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdGetCallCost) defaults() error {
|
||||
self.rpcMethod = "ApierV1.GetCallCostLog"
|
||||
self.rpcParams = &apier.AttrGetCallCost{RunId: utils.DEFAULT_RUNID}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses command line args and builds CmdBalance value
|
||||
func (self *CmdGetCallCost) FromArgs(args []string) error {
|
||||
if len(args) < 3 {
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
// Args look OK, set defaults before going further
|
||||
self.defaults()
|
||||
self.rpcParams.CgrId = args[2]
|
||||
if len(args) == 4 {
|
||||
self.rpcParams.RunId = args[3]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CmdGetCallCost) RpcMethod() string {
|
||||
return self.rpcMethod
|
||||
}
|
||||
|
||||
func (self *CmdGetCallCost) RpcParams() interface{} {
|
||||
return self.rpcParams
|
||||
}
|
||||
|
||||
func (self *CmdGetCallCost) RpcResult() interface{} {
|
||||
return &self.rpcResult
|
||||
}
|
||||
102
console/get_maxduration.go
Normal file
102
console/get_maxduration.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cgrates/cgrates/engine"
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["get_maxduration"] = &CmdGetMaxDuration{}
|
||||
}
|
||||
|
||||
// Commander implementation
|
||||
type CmdGetMaxDuration struct {
|
||||
rpcMethod string
|
||||
rpcParams *engine.CallDescriptor
|
||||
rpcResult *float64
|
||||
}
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdGetMaxDuration) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] get_maxduration <tor> <tenant> <subject> <destination> <start_time|*now> [<target_duration>]")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdGetMaxDuration) defaults() error {
|
||||
self.rpcMethod = "Responder.GetMaxSessionTime"
|
||||
self.rpcParams = &engine.CallDescriptor{Direction: "*out"}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses command line args and builds CmdBalance value
|
||||
func (self *CmdGetMaxDuration) FromArgs(args []string) error {
|
||||
if len(args) < 7 {
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
// Args look OK, set defaults before going further
|
||||
self.defaults()
|
||||
var tStart time.Time
|
||||
var err error
|
||||
if args[6] == "*now" {
|
||||
tStart = time.Now()
|
||||
} else {
|
||||
tStart, err = utils.ParseDate(args[6])
|
||||
if err != nil {
|
||||
fmt.Println("\n*start_time* should have one of the formats:")
|
||||
fmt.Println("\ttime.RFC3339\teg:2013-08-07T17:30:00Z in UTC")
|
||||
fmt.Println("\tunix time\teg: 1383823746")
|
||||
fmt.Println("\t*now\t\tmetafunction transformed into localtime at query time")
|
||||
fmt.Println("\t+dur\t\tduration to be added to localtime (valid suffixes: ns, us/µs, ms, s, m, h)\n")
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
}
|
||||
var callDur time.Duration
|
||||
if len(args) == 8 {
|
||||
callDur, err = utils.ParseDurationWithSecs(args[7])
|
||||
if err != nil {
|
||||
fmt.Println("\n\tExample durations: 60s for 60 seconds, 25m for 25minutes, 1m25s for one minute and 25 seconds\n")
|
||||
}
|
||||
} else { // Enforce call duration to a predefined 7200s
|
||||
callDur = time.Duration(7200) * time.Second
|
||||
}
|
||||
self.rpcParams.TOR = args[2]
|
||||
self.rpcParams.Tenant = args[3]
|
||||
self.rpcParams.Subject = args[4]
|
||||
self.rpcParams.Destination = args[5]
|
||||
self.rpcParams.TimeStart = tStart
|
||||
self.rpcParams.CallDuration = callDur
|
||||
self.rpcParams.TimeEnd = tStart.Add(callDur)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CmdGetMaxDuration) RpcMethod() string {
|
||||
return self.rpcMethod
|
||||
}
|
||||
|
||||
func (self *CmdGetMaxDuration) RpcParams() interface{} {
|
||||
return self.rpcParams
|
||||
}
|
||||
|
||||
func (self *CmdGetMaxDuration) RpcResult() interface{} {
|
||||
return &self.rpcResult
|
||||
}
|
||||
71
console/rem_cdrs.go
Normal file
71
console/rem_cdrs.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Rating system designed to be used in VoIP Carriers World
|
||||
Copyright (C) 2013 ITsysCOM
|
||||
|
||||
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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["rem_cdrs"] = &CmdRemCdrs{}
|
||||
}
|
||||
|
||||
// Commander implementation
|
||||
type CmdRemCdrs struct {
|
||||
rpcMethod string
|
||||
rpcParams *utils.AttrRemCdrs
|
||||
rpcResult string
|
||||
}
|
||||
|
||||
// name should be exec's name
|
||||
func (self *CmdRemCdrs) Usage(name string) string {
|
||||
return fmt.Sprintf("\n\tUsage: cgr-console [cfg_opts...{-h}] rem_cdrs <cgrid> [<cdrid> [<cdrid>...]]")
|
||||
}
|
||||
|
||||
// set param defaults
|
||||
func (self *CmdRemCdrs) defaults() error {
|
||||
self.rpcMethod = "ApierV1.RemCdrs"
|
||||
self.rpcParams = &utils.AttrRemCdrs{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses command line args and builds CmdBalance value
|
||||
func (self *CmdRemCdrs) FromArgs(args []string) error {
|
||||
if len(args) < 3 {
|
||||
return fmt.Errorf(self.Usage(""))
|
||||
}
|
||||
// Args look OK, set defaults before going further
|
||||
self.defaults()
|
||||
self.rpcParams.CgrIds = args[2:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CmdRemCdrs) RpcMethod() string {
|
||||
return self.rpcMethod
|
||||
}
|
||||
|
||||
func (self *CmdRemCdrs) RpcParams() interface{} {
|
||||
return self.rpcParams
|
||||
}
|
||||
|
||||
func (self *CmdRemCdrs) RpcResult() interface{} {
|
||||
return &self.rpcResult
|
||||
}
|
||||
@@ -24,40 +24,41 @@
|
||||
# stordb_user = cgrates # Username to use when connecting to stordb.
|
||||
# stordb_passwd = CGRateS.org # Password to use when connecting to stordb.
|
||||
# dbdata_encoding = msgpack # The encoding used to store object data in strings: <msgpack|json>
|
||||
# rpc_encoding = json # RPC encoding used on APIs: <gob|json>.
|
||||
# rpc_json_listen = 127.0.0.1:2012 # RPC JSON listening address
|
||||
# rpc_gob_listen = 127.0.0.1:2013 # RPC GOB listening address
|
||||
# http_listen = 127.0.0.1:2080 # HTTP listening address
|
||||
# default_reqtype = rated # Default request type to consider when missing from requests: <""|prepaid|postpaid|pseudoprepaid|rated>.
|
||||
# default_tor = call # Default Type of Record to consider when missing from requests.
|
||||
# default_tenant = cgrates.org # Default Tenant to consider when missing from requests.
|
||||
# default_subject = cgrates # Default rating Subject to consider when missing from requests.
|
||||
# rounding_method = *middle # Rounding method for floats/costs: <*up|*middle|*down>
|
||||
# rounding_decimals = 4 # Number of decimals to round float/costs at
|
||||
# xmlcfg_path = # Path towards additional config defined in xml file
|
||||
|
||||
[balancer]
|
||||
# enabled = false # Start Balancer service: <true|false>.
|
||||
# listen = 127.0.0.1:2012 # Balancer listen interface: <""|x.y.z.y:1234>.
|
||||
|
||||
[rater]
|
||||
# enabled = false # Enable RaterCDRSExportPath service: <true|false>.
|
||||
# balancer = disabled # Register to Balancer as worker: <enabled|disabled>.
|
||||
# listen = 127.0.0.1:2012 # Rater's listening interface: <internal|x.y.z.y:1234>.
|
||||
# balancer = # Register to Balancer as worker: <""|internal|127.0.0.1:2013>.
|
||||
|
||||
[scheduler]
|
||||
# enabled = false # Starts Scheduler service: <true|false>.
|
||||
|
||||
[cdrs]
|
||||
# enabled = false # Start the CDR Server service: <true|false>.
|
||||
# listen=127.0.0.1:2022 # CDRS's listening interface: <x.y.z.y:1234>.
|
||||
# extra_fields = # Extra fields to store in CDRs
|
||||
# extra_fields = # Extra fields to store in CDRs for non-generic CDRs
|
||||
# mediator = # Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
|
||||
|
||||
[cdre]
|
||||
# cdr_format = csv # Exported CDRs format <csv>
|
||||
# extra_fields = # List of extra fields to be exported out in CDRs
|
||||
# export_dir = /var/log/cgrates/cdr/cdrexport/csv # Path where the exported CDRs will be placed
|
||||
# export_template = cgrid,mediation_runid,accid,cdrhost,reqtype,direction,tenant,tor,account,subject,destination,setup_time,answer_time,duration,cost
|
||||
# Exported fields template <""|fld1,fld2|*xml:instance_name>
|
||||
|
||||
[cdrc]
|
||||
# enabled = false # Enable CDR client functionality
|
||||
# cdrs = 127.0.0.1:2022 # Address where to reach CDR server
|
||||
# cdrs = internal # Address where to reach CDR server. <internal|127.0.0.1:2080>
|
||||
# cdrs_method = http_cgr # Mechanism to use when posting CDRs on server <http_cgr>
|
||||
# run_delay = 0 # Sleep interval in seconds between consecutive runs, 0 to use automation via inotify
|
||||
# cdr_type = csv # CDR file format <csv|freeswitch_csv>.
|
||||
@@ -72,14 +73,14 @@
|
||||
# account_field = 5 # Account field identifier. Use index numbers in case of .csv cdrs.
|
||||
# subject_field = 6 # Subject field identifier. Use index numbers in case of .csv CDRs.
|
||||
# destination_field = 7 # Destination field identifier. Use index numbers in case of .csv cdrs.
|
||||
# answer_time_field = 8 # Answer time field identifier. Use index numbers in case of .csv cdrs.
|
||||
# duration_field = 9 # Duration field identifier. Use index numbers in case of .csv cdrs.
|
||||
# setup_time_field = 8 # Setup time field identifier. Use index numbers in case of .csv cdrs.
|
||||
# answer_time_field = 9 # Answer time field identifier. Use index numbers in case of .csv cdrs.
|
||||
# duration_field = 10 # Duration field identifier. Use index numbers in case of .csv cdrs.
|
||||
# extra_fields = # Extra fields identifiers. For .csv, format: <label_extrafield_1>:<index_extrafield_1>[...,<label_extrafield_n>:<index_extrafield_n>]
|
||||
|
||||
[mediator]
|
||||
# enabled = false # Starts Mediator service: <true|false>.
|
||||
# listen=internal # Mediator's listening interface: <internal>.
|
||||
# rater = 127.0.0.1:2012 # Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
# rater = internal # Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
# rater_reconnects = 3 # Number of reconnects to rater before giving up.
|
||||
# run_ids = # Identifiers of each extra mediation to run on CDRs
|
||||
# reqtype_fields = # Name of request type fields to be used during extra mediation. Use index number in case of .csv cdrs.
|
||||
@@ -89,16 +90,28 @@
|
||||
# account_fields = # Name of account fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
# subject_fields = # Name of fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
# destination_fields = # Name of destination fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
# answer_time_fields = # Name of time_answer fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
# setup_time_fields = # Name of setup_time fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
# answer_time_fields = # Name of answer_time fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
# duration_fields = # Name of duration fields to be used during extra mediation. Use index numbers in case of .csv cdrs.
|
||||
|
||||
[session_manager]
|
||||
# enabled = false # Starts SessionManager service: <true|false>.
|
||||
# switch_type = freeswitch # Defines the type of switch behind: <freeswitch>.
|
||||
# rater = 127.0.0.1:2012 # Address where to reach the Rater.
|
||||
# rater = internal # Address where to reach the Rater.
|
||||
# rater_reconnects = 3 # Number of reconnects to rater before giving up.
|
||||
# debit_interval = 10 # Interval to perform debits on.
|
||||
# max_call_duration = 3h # Maximum call duration a prepaid call can last
|
||||
# run_ids = # Identifiers of additional sessions control.
|
||||
# reqtype_fields = # Name of request type fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# direction_fields = # Name of direction fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# tenant_fields = # Name of tenant fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# tor_fields = # Name of tor fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# account_fields = # Name of account fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# subject_fields = # Name of fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# destination_fields = # Name of destination fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# setup_time_fields = # Name of setup_time fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# answer_time_fields = # Name of answer_time fields to be used during additional sessions control <""|*default|field_name>.
|
||||
# duration_fields = # Name of duration fields to be used during additional sessions control <""|*default|field_name>.
|
||||
|
||||
[freeswitch]
|
||||
# server = 127.0.0.1:8021 # Adress where to connect to FreeSWITCH socket.
|
||||
@@ -107,11 +120,16 @@
|
||||
|
||||
[history_server]
|
||||
# enabled = false # Starts History service: <true|false>.
|
||||
# listen = 127.0.0.1:2013 # Listening addres for history server: <internal|x.y.z.y:1234>
|
||||
# history_dir = /var/log/cgrates/history # Location on disk where to store history files.
|
||||
# save_interval = 1s # Interval to save changed cache into .git archive
|
||||
# save_interval = 1s # Interval to save changed cache into .git archive
|
||||
|
||||
[history_agent]
|
||||
# enabled = false # Starts History as a client: <true|false>.
|
||||
# server = 127.0.0.1:2013 # Address where to reach the master history server: <internal|x.y.z.y:1234>
|
||||
# server = internal # Address where to reach the master history server: <internal|x.y.z.y:1234>
|
||||
|
||||
[mailer]
|
||||
# server = localhost # The server to use when sending emails out
|
||||
# auth_user = cgrates # Authenticate to email server using this user
|
||||
# auth_passwd = CGRateS.org # Authenticate to email server with this password
|
||||
# from_address = cgr-mailer@localhost.localdomain # From address used when sending emails out
|
||||
|
||||
|
||||
35
data/conf/samples/apier_local_test.cfg
Normal file
35
data/conf/samples/apier_local_test.cfg
Normal file
@@ -0,0 +1,35 @@
|
||||
# CGRateS Configuration file
|
||||
#
|
||||
# This file contains the default configuration hardcoded into CGRateS.
|
||||
# This is what you get when you load CGRateS with an empty configuration file.
|
||||
# [global] must exist in all files, rest of the configuration is inter-changeable.
|
||||
|
||||
[rater]
|
||||
enabled = true # Enable RaterCDRSExportPath service: <true|false>.
|
||||
|
||||
[scheduler]
|
||||
enabled = true # Starts Scheduler service: <true|false>.
|
||||
|
||||
[cdrs]
|
||||
enabled = true # Start the CDR Server service: <true|false>.
|
||||
mediator = internal # Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
|
||||
|
||||
[cdre]
|
||||
export_dir = /tmp/cgrates/cdr/cdre/csv # Path where the exported CDRs will be placed
|
||||
|
||||
[cdrc]
|
||||
cdr_in_dir = /tmp/cgrates/cdr/cdrc/in # Absolute path towards the directory where the CDRs are stored.
|
||||
cdr_out_dir =/tmp/cgrates/cdr/cdrc/out # Absolute path towards the directory where processed CDRs will be moved.
|
||||
|
||||
[mediator]
|
||||
enabled = true # Starts Mediator service: <true|false>.
|
||||
rater = 127.0.0.1:2012 # Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
|
||||
[history_server]
|
||||
enabled = true # Starts History service: <true|false>.
|
||||
history_dir = /tmp/cgrates/history # Location on disk where to store history files.
|
||||
|
||||
[history_agent]
|
||||
enabled = true # Starts History as a client: <true|false>.
|
||||
|
||||
|
||||
20
data/conf/samples/cgr_addconfig.xml
Normal file
20
data/conf/samples/cgr_addconfig.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<document type="cgrates/xml">
|
||||
<configuration section="cdre" type="fixed_width" id="CDREFW-A">
|
||||
<header>
|
||||
<fields>
|
||||
<field name="Filler1" type="filler" width="4"/>
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="call"/>
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="Filler1" type="filler" width="3"/>
|
||||
</fields>
|
||||
</trailer>
|
||||
</configuration>
|
||||
</document>
|
||||
13
data/conf/samples/config_local_test.cfg
Normal file
13
data/conf/samples/config_local_test.cfg
Normal file
@@ -0,0 +1,13 @@
|
||||
# CGRateS Configuration file
|
||||
#
|
||||
# This file contains the default configuration hardcoded into CGRateS.
|
||||
# This is what you get when you load CGRateS with an empty configuration file.
|
||||
# [global] must exist in all files, rest of the configuration is inter-changeable.
|
||||
|
||||
[global]
|
||||
xmlcfg_path = /usr/share/cgrates/conf/samples/cgr_addconfig.xml # Path towards additional config defined in xml file
|
||||
|
||||
[cdre]
|
||||
cdr_format = fixed_width # Exported CDRs format <csv>
|
||||
export_template = *xml:CDREFW-A # Exported fields template <""|fld1,fld2|*xml:instance_name>
|
||||
|
||||
20
data/conf/samples/mediator_test1.cfg
Normal file
20
data/conf/samples/mediator_test1.cfg
Normal file
@@ -0,0 +1,20 @@
|
||||
# CGRateS Configuration file
|
||||
#
|
||||
# Used in mediator_local_test
|
||||
# Starts rater, cdrs and mediator connecting over internal channel
|
||||
|
||||
[rater]
|
||||
enabled = true # Enable RaterCDRSExportPath service: <true|false>.
|
||||
|
||||
[cdrs]
|
||||
enabled = true # Start the CDR Server service: <true|false>.
|
||||
mediator = internal # Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
|
||||
|
||||
[cdre]
|
||||
export_dir = /tmp/cgrates/cdr/cdrexport/csv # Path where the exported CDRs will be placed
|
||||
|
||||
[mediator]
|
||||
enabled = true # Starts Mediator service: <true|false>.
|
||||
rater = internal # Address where to reach the Rater: <internal|x.y.z.y:1234>
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<configuration name="abstraction.conf" description="Abstraction">
|
||||
<apis>
|
||||
<api name="user_name" description="Return Name for extension" syntax="<exten>" parse="(.*)" destination="user_data" argument="$1@default var effective_caller_id_name"/>
|
||||
</apis>
|
||||
</configuration>
|
||||
@@ -1,32 +0,0 @@
|
||||
<configuration name="acl.conf" description="Network Lists">
|
||||
<network-lists>
|
||||
<!--
|
||||
These ACL's are automatically created on startup.
|
||||
|
||||
rfc1918.auto - RFC1918 Space
|
||||
nat.auto - RFC1918 Excluding your local lan.
|
||||
localnet.auto - ACL for your local lan.
|
||||
loopback.auto - ACL for your local lan.
|
||||
-->
|
||||
|
||||
<list name="lan" default="allow">
|
||||
<node type="deny" cidr="192.168.42.0/24"/>
|
||||
<node type="allow" cidr="192.168.42.42/32"/>
|
||||
</list>
|
||||
|
||||
<!--
|
||||
This will traverse the directory adding all users
|
||||
with the cidr= tag to this ACL, when this ACL matches
|
||||
the users variables and params apply as if they
|
||||
digest authenticated.
|
||||
-->
|
||||
<list name="domains" default="deny">
|
||||
<!-- domain= is special it scans the domain from the directory to build the ACL -->
|
||||
<node type="allow" domain="$${domain}"/>
|
||||
<!-- use cidr= if you wish to allow ip ranges to this domains acl. -->
|
||||
<!-- <node type="allow" cidr="192.168.0.0/24"/> -->
|
||||
</list>
|
||||
|
||||
</network-lists>
|
||||
</configuration>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<configuration name="alsa.conf" description="Soundcard Endpoint">
|
||||
<settings>
|
||||
<!--Default dialplan and caller-id info -->
|
||||
<param name="dialplan" value="XML"/>
|
||||
<param name="cid-name" value="N800 Alsa"/>
|
||||
<param name="cid-num" value="5555551212"/>
|
||||
|
||||
<!--audio sample rate and interval -->
|
||||
<param name="sample-rate" value="8000"/>
|
||||
<param name="codec-ms" value="20"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,11 +0,0 @@
|
||||
<configuration name="mod_blacklist.conf" description="Blacklist module">
|
||||
<lists>
|
||||
<!--
|
||||
Example blacklist, the referenced file contains blacklisted items, one entry per line
|
||||
|
||||
NOTE: make sure the file exists and is readable by FreeSWITCH.
|
||||
|
||||
<list name="example" filename="/usr/local/freeswitch/conf/blacklists/example.list"/>
|
||||
-->
|
||||
</lists>
|
||||
</configuration>
|
||||
@@ -1,38 +0,0 @@
|
||||
<configuration name="callcenter.conf" description="CallCenter">
|
||||
<settings>
|
||||
<!--<param name="odbc-dsn" value="dsn:user:pass"/>-->
|
||||
<!--<param name="dbname" value="/dev/shm/callcenter.db"/>-->
|
||||
</settings>
|
||||
|
||||
<queues>
|
||||
|
||||
<queue name="support@default">
|
||||
<param name="strategy" value="longest-idle-agent"/>
|
||||
<param name="moh-sound" value="$${hold_music}"/>
|
||||
<!--<param name="record-template" value="$${base_dir}/recordings/${strftime(%Y-%m-%d-%H-%M-%S)}.${destination_number}.${caller_id_number}.${uuid}.wav"/>-->
|
||||
<param name="time-base-score" value="system"/>
|
||||
<param name="max-wait-time" value="0"/>
|
||||
<param name="max-wait-time-with-no-agent" value="0"/>
|
||||
<param name="max-wait-time-with-no-agent-time-reached" value="5"/>
|
||||
<param name="tier-rules-apply" value="false"/>
|
||||
<param name="tier-rule-wait-second" value="300"/>
|
||||
<param name="tier-rule-wait-multiply-level" value="true"/>
|
||||
<param name="tier-rule-no-agent-no-wait" value="false"/>
|
||||
<param name="discard-abandoned-after" value="60"/>
|
||||
<param name="abandoned-resume-allowed" value="false"/>
|
||||
</queue>
|
||||
|
||||
</queues>
|
||||
|
||||
<!-- WARNING: Configuration of XML Agents will be updated into the DB upon restart. -->
|
||||
<!-- WARNING: Configuration of XML Tiers will reset the level and position if those were supplied. -->
|
||||
<!-- WARNING: Agents and Tiers XML config shouldn't be used in a multi FS shared DB setup (Not currently supported anyway) -->
|
||||
<agents>
|
||||
<!--<agent name="1000@default" type="callback" contact="[call_timeout=10]user/1000@default" status="Available" max-no-answer="3" wrap-up-time="10" reject-delay-time="10" busy-delay-time="60" />-->
|
||||
</agents>
|
||||
<tiers>
|
||||
<!-- If no level or position is provided, they will default to 1. You should do this to keep db value on restart. -->
|
||||
<!-- <tier agent="1000@default" queue="support@default" level="1" position="1"/> -->
|
||||
</tiers>
|
||||
|
||||
</configuration>
|
||||
@@ -1,13 +0,0 @@
|
||||
<configuration name="cdr_mongodb.conf" description="MongoDB CDR logger">
|
||||
<settings>
|
||||
<!-- Hostnames and IPv6 addrs not supported (yet) -->
|
||||
<param name="host" value="127.0.0.1"/>
|
||||
<param name="port" value="27017"/>
|
||||
|
||||
<!-- Namespace format is database.collection -->
|
||||
<param name="namespace" value="test.cdr"/>
|
||||
|
||||
<!-- If true, create CDR for B-leg of call (default: true) -->
|
||||
<param name="log-b-leg" value="false"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,40 +0,0 @@
|
||||
<configuration name="cdr_pg_csv.conf" description="CDR PG CSV Format">
|
||||
<settings>
|
||||
<!-- See parameters for PQconnectdb() at http://www.postgresql.org/docs/8.4/static/libpq-connect.html -->
|
||||
<param name="db-info" value="host=localhost dbname=cdr connect_timeout=10" />
|
||||
<!-- CDR table name -->
|
||||
<!--<param name="db-table" value="cdr"/>-->
|
||||
|
||||
<!-- Log a-leg (a), b-leg (b) or both (ab) -->
|
||||
<param name="legs" value="a"/>
|
||||
|
||||
<!-- Directory in which to spool failed SQL inserts -->
|
||||
<!-- <param name="spool-dir" value="$${base_dir}/log/cdr-pg-csv"/> -->
|
||||
<!-- Disk spool format if DB connection/insert fails - csv (default) or sql -->
|
||||
<param name="spool-format" value="csv"/>
|
||||
<param name="rotate-on-hup" value="true"/>
|
||||
|
||||
<!-- This is like the info app but after the call is hung up -->
|
||||
<!--<param name="debug" value="true"/>-->
|
||||
</settings>
|
||||
<schema>
|
||||
<field var="local_ip_v4"/>
|
||||
<field var="caller_id_name"/>
|
||||
<field var="caller_id_number"/>
|
||||
<field var="destination_number"/>
|
||||
<field var="context"/>
|
||||
<field var="start_stamp"/>
|
||||
<field var="answer_stamp"/>
|
||||
<field var="end_stamp"/>
|
||||
<field var="duration" quote="false"/>
|
||||
<field var="billsec" quote="false"/>
|
||||
<field var="hangup_cause"/>
|
||||
<field var="uuid"/>
|
||||
<field var="bleg_uuid"/>
|
||||
<field var="accountcode"/>
|
||||
<field var="read_codec"/>
|
||||
<field var="write_codec"/>
|
||||
<!-- <field var="sip_hangup_disposition"/> -->
|
||||
<!-- <field var="ani"/> -->
|
||||
</schema>
|
||||
</configuration>
|
||||
@@ -1,18 +0,0 @@
|
||||
<configuration name="cdr_sqlite.conf" description="SQLite CDR">
|
||||
<settings>
|
||||
<!-- SQLite database name (.db suffix will be automatically appended) -->
|
||||
<!-- <param name="db-name" value="cdr"/> -->
|
||||
<!-- CDR table name -->
|
||||
<!-- <param name="db-table" value="cdr"/> -->
|
||||
<!-- Log a-leg (a), b-leg (b) or both (ab) -->
|
||||
<param name="legs" value="a"/>
|
||||
<!-- Default template to use when inserting records -->
|
||||
<param name="default-template" value="example"/>
|
||||
<!-- This is like the info app but after the call is hung up -->
|
||||
<!--<param name="debug" value="true"/>-->
|
||||
</settings>
|
||||
<templates>
|
||||
<!-- Note that field order must match SQL table schema, otherwise insert will fail -->
|
||||
<template name="example">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}",${duration},${billsec},"${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}"</template>
|
||||
</templates>
|
||||
</configuration>
|
||||
@@ -1,12 +0,0 @@
|
||||
<configuration name="cepstral.conf" description="Cepstral TTS configuration">
|
||||
<settings>
|
||||
<!--
|
||||
Possible encodings:
|
||||
* utf-8
|
||||
* us-ascii
|
||||
* iso8859-1 (default)
|
||||
* iso8859-15
|
||||
-->
|
||||
<param name="encoding" value="utf-8"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,33 +0,0 @@
|
||||
<configuration name="cidlookup.conf" description="cidlookup Configuration">
|
||||
<settings>
|
||||
<!-- comment out url to not setup a url based lookup -->
|
||||
<param name="url" value="http://query.voipcnam.com/query.php?api_key=MYAPIKEY&number=${caller_id_number}"/>
|
||||
|
||||
<!-- comment out whitepages-apikey to not use whitepages.com, you must
|
||||
get an API key from http://developer.whitepages.com/ -->
|
||||
<param name="whitepages-apikey" value="MYAPIKEY"/>
|
||||
|
||||
<!-- set to false to not cache (in memcache) results from the url query -->
|
||||
<param name="cache" value="true"/>
|
||||
<!-- expire is in seconds -->
|
||||
<param name="cache-expire" value="86400"/>
|
||||
|
||||
<param name="odbc-dsn" value="phone:phone:phone"/>
|
||||
|
||||
<!-- comment out sql to not setup a database (directory) lookup -->
|
||||
<param name="sql" value="
|
||||
SELECT name||' ('||type||')' AS name
|
||||
FROM phonebook p JOIN numbers n ON p.id = n.phonebook_id
|
||||
WHERE n.number='${caller_id_number}'
|
||||
LIMIT 1
|
||||
"/>
|
||||
<!-- comment out citystate-sql to not setup a database (city/state)
|
||||
lookup -->
|
||||
<param name="citystate-sql" value="
|
||||
SELECT ratecenter||' '||state as name
|
||||
FROM npa_nxx_company_ocn
|
||||
WHERE npa = ${caller_id_number:1:3} AND nxx = ${caller_id_number:4:3}
|
||||
LIMIT 1
|
||||
"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,213 +0,0 @@
|
||||
<!-- http://wiki.freeswitch.org/wiki/Mod_conference -->
|
||||
<!-- None of these paths are real if you want any of these options you need to really set them up -->
|
||||
<configuration name="conference.conf" description="Audio Conference">
|
||||
<!-- Advertise certain presence on startup . -->
|
||||
<advertise>
|
||||
<room name="3001@$${domain}" status="FreeSWITCH"/>
|
||||
</advertise>
|
||||
|
||||
<!-- These are the default keys that map when you do not specify a caller control group -->
|
||||
<!-- Note: none and default are reserved names for group names. Disabled if dist-dtmf member flag is set. -->
|
||||
<caller-controls>
|
||||
<group name="default">
|
||||
<control action="mute" digits="0"/>
|
||||
<control action="deaf mute" digits="*"/>
|
||||
<control action="energy up" digits="9"/>
|
||||
<control action="energy equ" digits="8"/>
|
||||
<control action="energy dn" digits="7"/>
|
||||
<control action="vol talk up" digits="3"/>
|
||||
<control action="vol talk zero" digits="2"/>
|
||||
<control action="vol talk dn" digits="1"/>
|
||||
<control action="vol listen up" digits="6"/>
|
||||
<control action="vol listen zero" digits="5"/>
|
||||
<control action="vol listen dn" digits="4"/>
|
||||
<control action="hangup" digits="#"/>
|
||||
</group>
|
||||
</caller-controls>
|
||||
|
||||
<!-- Profiles are collections of settings you can reference by name. -->
|
||||
<profiles>
|
||||
<!--If no profile is specified it will default to "default"-->
|
||||
<profile name="default">
|
||||
<!-- Directory to drop CDR's
|
||||
'auto' means $PREFIX/logs/conference_cdr/<confernece_uuid>.cdr.xml
|
||||
a non-absolute path means $PREFIX/logs/<value>/<confernece_uuid>.cdr.xml
|
||||
absolute path means <value>/<confernece_uuid>.cdr.xml
|
||||
-->
|
||||
<!-- <param name="cdr-log-dir" value="auto"/> -->
|
||||
|
||||
<!-- Domain (for presence) -->
|
||||
<param name="domain" value="$${domain}"/>
|
||||
<!-- Sample Rate-->
|
||||
<param name="rate" value="8000"/>
|
||||
<!-- Number of milliseconds per frame -->
|
||||
<param name="interval" value="20"/>
|
||||
<!-- Energy level required for audio to be sent to the other users -->
|
||||
<param name="energy-level" value="300"/>
|
||||
|
||||
<!--Can be | delim of waste|mute|deaf|dist-dtmf waste will always transmit data to each channel
|
||||
even during silence. dist-dtmf propagates dtmfs to all other members, but channel controls
|
||||
via dtmf will be disabled. -->
|
||||
<!--<param name="member-flags" value="waste"/>-->
|
||||
|
||||
<!-- Name of the caller control group to use for this profile -->
|
||||
<!-- <param name="caller-controls" value="some name"/> -->
|
||||
<!-- Name of the caller control group to use for the moderator in this profile -->
|
||||
<!-- <param name="moderator-controls" value="some name"/> -->
|
||||
<!-- TTS Engine to use -->
|
||||
<!--<param name="tts-engine" value="cepstral"/>-->
|
||||
<!-- TTS Voice to use -->
|
||||
<!--<param name="tts-voice" value="david"/>-->
|
||||
|
||||
<!-- If TTS is enabled all audio-file params beginning with -->
|
||||
<!-- 'say:' will be considered text to say with TTS -->
|
||||
<!-- Override the default path here, after which you use relative paths in the other sound params -->
|
||||
<!-- Note: The default path is the conference's first caller's sound_prefix -->
|
||||
<!--<param name="sound-prefix" value="$${sounds_dir}/en/us/callie"/>-->
|
||||
<!-- File to play to acknowledge succees -->
|
||||
<!--<param name="ack-sound" value="beep.wav"/>-->
|
||||
<!-- File to play to acknowledge failure -->
|
||||
<!--<param name="nack-sound" value="beeperr.wav"/>-->
|
||||
<!-- File to play to acknowledge muted -->
|
||||
<param name="muted-sound" value="conference/conf-muted.wav"/>
|
||||
<!-- File to play to acknowledge unmuted -->
|
||||
<param name="unmuted-sound" value="conference/conf-unmuted.wav"/>
|
||||
<!-- File to play if you are alone in the conference -->
|
||||
<param name="alone-sound" value="conference/conf-alone.wav"/>
|
||||
<!-- File to play endlessly (nobody will ever be able to talk) -->
|
||||
<!--<param name="perpetual-sound" value="perpetual.wav"/>-->
|
||||
<!-- File to play when you're alone (music on hold)-->
|
||||
<param name="moh-sound" value="$${hold_music}"/>
|
||||
<!-- File to play when you join the conference -->
|
||||
<param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/>
|
||||
<!-- File to play when you leave the conference -->
|
||||
<param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/>
|
||||
<!-- File to play when you ae ejected from the conference -->
|
||||
<param name="kicked-sound" value="conference/conf-kicked.wav"/>
|
||||
<!-- File to play when the conference is locked -->
|
||||
<param name="locked-sound" value="conference/conf-locked.wav"/>
|
||||
<!-- File to play when the conference is locked during the call-->
|
||||
<param name="is-locked-sound" value="conference/conf-is-locked.wav"/>
|
||||
<!-- File to play when the conference is unlocked during the call-->
|
||||
<param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/>
|
||||
<!-- File to play to prompt for a pin -->
|
||||
<param name="pin-sound" value="conference/conf-pin.wav"/>
|
||||
<!-- File to play to when the pin is invalid -->
|
||||
<param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/>
|
||||
<!-- Conference pin -->
|
||||
<!--<param name="pin" value="12345"/>-->
|
||||
<!--<param name="moderator-pin" value="54321"/>-->
|
||||
<!-- Max number of times the user can be prompted for PIN -->
|
||||
<!--<param name="pin-retries" value="3"/>-->
|
||||
<!-- Default Caller ID Name for outbound calls -->
|
||||
<param name="caller-id-name" value="$${outbound_caller_name}"/>
|
||||
<!-- Default Caller ID Number for outbound calls -->
|
||||
<param name="caller-id-number" value="$${outbound_caller_id}"/>
|
||||
<!-- Suppress start and stop talking events -->
|
||||
<!-- <param name="suppress-events" value="start-talking,stop-talking"/> -->
|
||||
<!-- enable comfort noise generation -->
|
||||
<param name="comfort-noise" value="true"/>
|
||||
<!-- Uncomment auto-record to toggle recording every conference call. -->
|
||||
<!-- Another valid value is shout://user:pass@server.com/live.mp3 -->
|
||||
<!--
|
||||
<param name="auto-record" value="$${recordings_dir}/${conference_name}_${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
|
||||
-->
|
||||
|
||||
<!-- IVR digit machine timeouts -->
|
||||
<!-- How much to wait between DTMF digits to match caller-controls -->
|
||||
<!-- <param name="ivr-dtmf-timeout" value="500"/> -->
|
||||
<!-- How much to wait for the first DTMF, 0 forever -->
|
||||
<!-- <param name="ivr-input-timeout" value="0" /> -->
|
||||
<!-- Delay before a conference is asked to be terminated -->
|
||||
<!-- <param name="endconf-grace-time" value="120" /> -->
|
||||
<!-- Can be | delim of wait-mod|audio-always|video-bridge|video-floor-only
|
||||
wait_mod will wait until the moderator in,
|
||||
audio-always will always mix audio from all members regardless they are talking or not -->
|
||||
<!-- <param name="conference-flags" value="audio-always"/> -->
|
||||
</profile>
|
||||
|
||||
<profile name="wideband">
|
||||
<param name="domain" value="$${domain}"/>
|
||||
<param name="rate" value="16000"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="energy-level" value="300"/>
|
||||
<!--<param name="sound-prefix" value="$${sounds_dir}/en/us/callie"/>-->
|
||||
<param name="muted-sound" value="conference/conf-muted.wav"/>
|
||||
<param name="unmuted-sound" value="conference/conf-unmuted.wav"/>
|
||||
<param name="alone-sound" value="conference/conf-alone.wav"/>
|
||||
<param name="moh-sound" value="$${hold_music}"/>
|
||||
<param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/>
|
||||
<param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/>
|
||||
<param name="kicked-sound" value="conference/conf-kicked.wav"/>
|
||||
<param name="locked-sound" value="conference/conf-locked.wav"/>
|
||||
<param name="is-locked-sound" value="conference/conf-is-locked.wav"/>
|
||||
<param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/>
|
||||
<param name="pin-sound" value="conference/conf-pin.wav"/>
|
||||
<param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/>
|
||||
<param name="caller-id-name" value="$${outbound_caller_name}"/>
|
||||
<param name="caller-id-number" value="$${outbound_caller_id}"/>
|
||||
<param name="comfort-noise" value="true"/>
|
||||
<!--<param name="tts-engine" value="flite"/>-->
|
||||
<!--<param name="tts-voice" value="kal16"/>-->
|
||||
</profile>
|
||||
|
||||
<profile name="ultrawideband">
|
||||
<param name="domain" value="$${domain}"/>
|
||||
<param name="rate" value="32000"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="energy-level" value="300"/>
|
||||
<!--<param name="sound-prefix" value="$${sounds_dir}/en/us/callie"/>-->
|
||||
<param name="muted-sound" value="conference/conf-muted.wav"/>
|
||||
<param name="unmuted-sound" value="conference/conf-unmuted.wav"/>
|
||||
<param name="alone-sound" value="conference/conf-alone.wav"/>
|
||||
<param name="moh-sound" value="$${hold_music}"/>
|
||||
<param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/>
|
||||
<param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/>
|
||||
<param name="kicked-sound" value="conference/conf-kicked.wav"/>
|
||||
<param name="locked-sound" value="conference/conf-locked.wav"/>
|
||||
<param name="is-locked-sound" value="conference/conf-is-locked.wav"/>
|
||||
<param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/>
|
||||
<param name="pin-sound" value="conference/conf-pin.wav"/>
|
||||
<param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/>
|
||||
<param name="caller-id-name" value="$${outbound_caller_name}"/>
|
||||
<param name="caller-id-number" value="$${outbound_caller_id}"/>
|
||||
<param name="comfort-noise" value="true"/>
|
||||
<!--<param name="tts-engine" value="flite"/>-->
|
||||
<!--<param name="tts-voice" value="kal16"/>-->
|
||||
</profile>
|
||||
|
||||
<profile name="cdquality">
|
||||
<param name="domain" value="$${domain}"/>
|
||||
<param name="rate" value="48000"/>
|
||||
<param name="interval" value="10"/>
|
||||
<param name="energy-level" value="300"/>
|
||||
<!--<param name="sound-prefix" value="$${sounds_dir}/en/us/callie"/>-->
|
||||
<param name="muted-sound" value="conference/conf-muted.wav"/>
|
||||
<param name="unmuted-sound" value="conference/conf-unmuted.wav"/>
|
||||
<param name="alone-sound" value="conference/conf-alone.wav"/>
|
||||
<param name="moh-sound" value="$${hold_music}"/>
|
||||
<param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/>
|
||||
<param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/>
|
||||
<param name="kicked-sound" value="conference/conf-kicked.wav"/>
|
||||
<param name="locked-sound" value="conference/conf-locked.wav"/>
|
||||
<param name="is-locked-sound" value="conference/conf-is-locked.wav"/>
|
||||
<param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/>
|
||||
<param name="pin-sound" value="conference/conf-pin.wav"/>
|
||||
<param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/>
|
||||
<param name="caller-id-name" value="$${outbound_caller_name}"/>
|
||||
<param name="caller-id-number" value="$${outbound_caller_id}"/>
|
||||
<param name="comfort-noise" value="true"/>
|
||||
</profile>
|
||||
|
||||
<profile name="sla">
|
||||
<param name="domain" value="$${domain}"/>
|
||||
<param name="rate" value="16000"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="caller-controls" value="none"/>
|
||||
<param name="energy-level" value="200"/>
|
||||
<param name="moh-sound" value="silence"/>
|
||||
<param name="comfort-noise" value="true"/>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
</configuration>
|
||||
@@ -1,56 +0,0 @@
|
||||
<configuration name="console.conf" description="Console Logger">
|
||||
<!-- pick a file name, a function name or 'all' -->
|
||||
<!-- map as many as you need for specific debugging -->
|
||||
<mappings>
|
||||
<!--
|
||||
name can be a file name, function name or 'all'
|
||||
value is one or more of debug,info,notice,warning,err,crit,alert,all
|
||||
See examples below
|
||||
|
||||
|
||||
The following map is the default, which is all debug levels enabled:
|
||||
<map name="all" value="debug,info,notice,warning,err,crit,alert"/>
|
||||
|
||||
|
||||
Example: the following turns on debugging for error and critical levels only
|
||||
<map name="all" value="err,crit"/>
|
||||
|
||||
NOTE: using map name="all" will override any other settings! If you
|
||||
want a more specific set of console messages then you will need
|
||||
to specify which files and/or functions you want to have debug
|
||||
messages. One option is to turn on just the more critical
|
||||
messages with map name="all", then specify the other types of
|
||||
console messages you want to see for various files and functions.
|
||||
|
||||
Example: turn on ERROR, CRIT, ALERT for all modules, then specify other
|
||||
levels for various modules and functions
|
||||
|
||||
<map name="all" value="err,crit,alert"/>
|
||||
<map name="switch_loadable_module_process" value="all"/>
|
||||
<map name="mod_local_stream.c" value="warning,debug"/>
|
||||
<map name="mod_sndfile.c" value="warning,info,debug"/>
|
||||
-->
|
||||
<map name="all" value="console,debug,info,notice,warning,err,crit,alert"/>
|
||||
|
||||
<!--
|
||||
You can use or modify this sample set of mappings. It turns on higher
|
||||
level messages for all modules and then specifies extra lower level
|
||||
messages for OpenZAP, Sofia, and switch core messages.
|
||||
|
||||
<map name="all" value="warning,err,crit,alert"/>
|
||||
<map name="zap_analog.c" value="all"/>
|
||||
<map name="zap_io.c" value="all"/>
|
||||
<map name="zap_isdn.c" value="all"/>
|
||||
<map name="zap_zt.c" value="all"/>
|
||||
<map name="mod_openzap" value="all"/>
|
||||
<map name="sofia.c" value="notice"/>
|
||||
<map name="switch_core_state_machine.c" value="all"/>
|
||||
|
||||
-->
|
||||
</mappings>
|
||||
<settings>
|
||||
<!-- comment or set to false for no color logging -->
|
||||
<param name="colorize" value="true"/>
|
||||
<param name="loglevel" value="$${console_loglevel}"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,5 +0,0 @@
|
||||
<configuration name="db.conf" description="LIMIT DB Configuration">
|
||||
<settings>
|
||||
<!--<param name="odbc-dsn" value="dsn:user:pass"/>-->
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,9 +0,0 @@
|
||||
<configuration name="dialplan_directory.conf" description="Dialplan Directory">
|
||||
<settings>
|
||||
<param name="directory-name" value="ldap"/>
|
||||
<param name="host" value="ldap.freeswitch.org"/>
|
||||
<param name="dn" value="cn=Manager,dc=freeswitch,dc=org"/>
|
||||
<param name="pass" value="test"/>
|
||||
<param name="base" value="dc=freeswitch,dc=org"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,9 +0,0 @@
|
||||
<configuration name="dingaling.conf" description="XMPP Jingle Endpoint">
|
||||
<settings>
|
||||
<param name="debug" value="0"/>
|
||||
<param name="codec-prefs" value="H264,PCMU"/>
|
||||
</settings>
|
||||
|
||||
<X-PRE-PROCESS cmd="include" data="../jingle_profiles/*.xml"/>
|
||||
|
||||
</configuration>
|
||||
@@ -1,21 +0,0 @@
|
||||
<configuration name="directory.conf" description="Directory">
|
||||
<settings>
|
||||
<!--<param name="odbc-dsn" value="dsn:user:pass"/>-->
|
||||
<!--<param name="dbname" value="directory"/>-->
|
||||
</settings>
|
||||
<profiles>
|
||||
<profile name="default">
|
||||
<param name="max-menu-attempts" value="3"/>
|
||||
<param name="min-search-digits" value="3"/>
|
||||
<param name="terminator-key" value="#"/>
|
||||
<param name="digit-timeout" value="3000"/>
|
||||
<param name="max-result" value="5"/>
|
||||
<param name="next-key" value="6"/>
|
||||
<param name="prev-key" value="4"/>
|
||||
<param name="switch-order-key" value="*"/>
|
||||
<param name="select-name-key" value="1"/>
|
||||
<param name="new-search-key" value="3"/>
|
||||
<param name="search-order" value="last_name"/>
|
||||
</profile>
|
||||
</profiles>
|
||||
</configuration>
|
||||
@@ -1,10 +0,0 @@
|
||||
<configuration name="distributor.conf" description="Distributor Configuration">
|
||||
<lists>
|
||||
<!-- every 10 calls to test you will get foo1 once and foo2 9 times...yes NINE TIMES! -->
|
||||
<!-- this is not the same as 100 with 10 and 90 that would do foo1 10 times in a row then foo2 90 times in a row -->
|
||||
<list name="test" total-weight="10">
|
||||
<node name="foo1" weight="1"/>
|
||||
<node name="foo2" weight="9"/>
|
||||
</list>
|
||||
</lists>
|
||||
</configuration>
|
||||
@@ -1,28 +0,0 @@
|
||||
<configuration name="easyroute.conf" description="EasyRoute Module">
|
||||
<settings>
|
||||
<!-- These are kind Obvious -->
|
||||
<param name="db-username" value="root"/>
|
||||
<param name="db-password" value="password"/>
|
||||
<param name="db-dsn" value="easyroute"/>
|
||||
|
||||
<!-- Default Technology and profile -->
|
||||
<param name="default-techprofile" value="sofia/default"/>
|
||||
|
||||
<!-- IP or Hostname of Default Route -->
|
||||
<param name="default-gateway" value="192.168.66.6"/>
|
||||
|
||||
<!-- Number of times to retry ODBC connection on connection problems, default is 120 -->
|
||||
<param name="odbc-retries" value="120"/>
|
||||
|
||||
<!-- Customer Query. Use this with Care!!! We are not responsible if you mess
|
||||
This up!!! Query *MUST* return columns in the following order!
|
||||
gateway varchar(128) - contains destination gateway host:port pair (ex: 192.168.1.1:5060 )
|
||||
group varchar(128) - contains optional group name
|
||||
call_limit varchar(16) - contains optional call limit
|
||||
tech_prefix varchar(128) - tech prefix used to build dial string (ex: sofia/default )
|
||||
acctcode varchar(128) - used to set channel variable acctcode for logging into the CDRs
|
||||
destination_number varchar(16) - Number returning for the query for building the dial string. (ex: 18005551212)
|
||||
See Documentation on the Wiki for further information -->
|
||||
<!-- <param name="custom-query" value="call FS_GET_SIP_LOCATION(%s);"/> -->
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,21 +0,0 @@
|
||||
<configuration name="enum.conf" description="ENUM Module">
|
||||
<settings>
|
||||
<param name="default-root" value="e164.org"/>
|
||||
<param name="default-isn-root" value="freenum.org"/>
|
||||
<param name="auto-reload" value="true"/>
|
||||
|
||||
<param name="query-timeout-ms" value="200"/>
|
||||
<param name="query-timeout-retry" value="2"/>
|
||||
<param name="random-nameserver" value="false"/>
|
||||
|
||||
<!-- If you have specific (non-recursive) servers for your enum queries, specify them here ( up to 10 ) -->
|
||||
<!-- <param name="nameserver" value="x.x.x.x"/> -->
|
||||
<!-- <param name="nameserver" value="y.y.y.y"/> -->
|
||||
</settings>
|
||||
|
||||
<routes>
|
||||
<route service="E2U+SIP" regex="sip:(.*)" replace="sofia/${use_profile}/$1;transport=udp"/>
|
||||
<route service="E2T+SIP" regex="sip:(.*)" replace="sofia/${use_profile}/$1;transport=tcp"/>
|
||||
<!--<route service="E2U+XMPP" regex="XMPP:(.*)" replace="dingaling/$${xmpp_server_profile}/$1"/>-->
|
||||
</routes>
|
||||
</configuration>
|
||||
@@ -1,23 +0,0 @@
|
||||
<configuration name="erlang_event.conf" description="Erlang Socket Client">
|
||||
<settings>
|
||||
<param name="listen-ip" value="0.0.0.0"/>
|
||||
<param name="listen-port" value="8031"/>
|
||||
<!-- Specify the first part of the node name
|
||||
(the host part after the @ will be autodetected)
|
||||
OR pass a complete nodename to avoid autodetection
|
||||
eg. freeswitch@example or freeswitch@example.com.
|
||||
If you pass a complete node name, the 'shortname' parameter has no effect. -->
|
||||
<param name="nodename" value="freeswitch"/>
|
||||
<!-- Specify this OR 'cookie-file' or $HOME/.erlang.cookie will be read -->
|
||||
<param name="cookie" value="ClueCon"/>
|
||||
<!-- Read a cookie from an arbitary erlang cookie file instead -->
|
||||
<!--<param name="cookie-file" value="/tmp/erlang.cookie"/>-->
|
||||
<param name="shortname" value="true"/>
|
||||
<!-- in additon to cookie, optionally restrict by ACL -->
|
||||
<!--<param name="apply-inbound-acl" value="lan"/>-->
|
||||
<!-- alternative is "binary" -->
|
||||
<!--<param name="encoding" value="string"/>-->
|
||||
<!-- provide compatability with previous OTP release (use with care) -->
|
||||
<!--<param name="compat-rel" value="12"/> -->
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,14 +0,0 @@
|
||||
<configuration name="event_multicast.conf" description="Multicast Event">
|
||||
<settings>
|
||||
<param name="address" value="225.1.1.1"/>
|
||||
<param name="port" value="4242"/>
|
||||
<param name="bindings" value="all"/>
|
||||
<param name="ttl" value="1"/>
|
||||
<!-- <param name="loopback" value="no"/>-->
|
||||
<!-- Uncomment this to enable pre-shared key encryption on the packets. -->
|
||||
<!-- For this option to work, you'll need to have the openssl development -->
|
||||
<!-- headers installed when you ran ./configure -->
|
||||
<!-- <param name="psk" value="ClueCon"/> -->
|
||||
</settings>
|
||||
</configuration>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<configuration name="event_socket.conf" description="Socket Client">
|
||||
<settings>
|
||||
<param name="nat-map" value="false"/>
|
||||
<param name="listen-ip" value="127.0.0.1"/>
|
||||
<param name="listen-port" value="8021"/>
|
||||
<param name="password" value="ClueCon"/>
|
||||
<!--<param name="apply-inbound-acl" value="lan"/>-->
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,12 +0,0 @@
|
||||
<configuration name="fax.conf" description="FAX application configuration">
|
||||
<settings>
|
||||
<param name="use-ecm" value="true"/>
|
||||
<param name="verbose" value="false"/>
|
||||
<param name="disable-v17" value="false"/>
|
||||
<param name="ident" value="SpanDSP Fax Ident"/>
|
||||
<param name="header" value="SpanDSP Fax Header"/>
|
||||
|
||||
<param name="spool-dir" value="/tmp"/>
|
||||
<param name="file-prefix" value="faxrx"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,10 +0,0 @@
|
||||
<configuration name="fifo.conf" description="FIFO Configuration">
|
||||
<settings>
|
||||
<param name="delete-all-outbound-member-on-startup" value="false"/>
|
||||
</settings>
|
||||
<fifos>
|
||||
<fifo name="cool_fifo@$${domain}" importance="0">
|
||||
<!--<member timeout="60" simo="1" lag="20">{member_wait=nowait}user/1005@$${domain}</member>-->
|
||||
</fifo>
|
||||
</fifos>
|
||||
</configuration>
|
||||
@@ -1,6 +0,0 @@
|
||||
<configuration name="hash.conf" description="Hash Configuration">
|
||||
<remotes>
|
||||
<!-- List of hosts from where to pull usage data -->
|
||||
<!-- <remote name="Test1" host="10.0.0.10" port="8021" password="ClueCon" interval="1000" /> -->
|
||||
</remotes>
|
||||
</configuration>
|
||||
@@ -1,127 +0,0 @@
|
||||
<configuration name="httapi.conf" description="HT-TAPI Hypertext Telephony API">
|
||||
<settings>
|
||||
<!-- print xml on the consol -->
|
||||
<param name="debug" value="true"/>
|
||||
<!-- time to keep audio files when discoverd they were deleted from the http server -->
|
||||
<param name="file-not-found-expires" value="300"/>
|
||||
<!-- how often to re-check the server to make sure the remote file has not changed -->
|
||||
<param name="file-cache-ttl" value="300"/>
|
||||
</settings>
|
||||
<profiles>
|
||||
<profile name="default">
|
||||
|
||||
<!-- default params for conference action tags -->
|
||||
<conference>
|
||||
<param name="default-profile" value="default"/>
|
||||
</conference>
|
||||
|
||||
<!-- default params for dial action tags -->
|
||||
<dial>
|
||||
<param name="context" value="default"/>
|
||||
<param name="dialplan" value="XML"/>
|
||||
</dial>
|
||||
|
||||
<!-- permissions -->
|
||||
<permissions>
|
||||
<!-- <permission name="all" value="true"/> -->
|
||||
<!--<permission name="none" value="true"/> -->
|
||||
<permission name="set-params" value="true"/>
|
||||
<permission name="set-vars" value="false">
|
||||
<!-- default to "deny" or "allow" -->
|
||||
<!-- type attr can be "deny" or "allow" nothing defaults to opposite of the list default so allow in this case -->
|
||||
<!--
|
||||
<variable-list default="deny">
|
||||
<variable name="caller_id_name"/>
|
||||
<variable name="hangup"/>
|
||||
</variable-list>
|
||||
-->
|
||||
</permission>
|
||||
<permission name="get-vars" value="false">
|
||||
<!-- default to "deny" or "allow" -->
|
||||
<!-- type attr can be "deny" or "allow" nothing defaults to opposite of the list default so allow in this case -->
|
||||
<!--
|
||||
<variable-list default="deny">
|
||||
<variable name="caller_id_name"/>
|
||||
<variable name="hangup"/>
|
||||
</variable-list>
|
||||
-->
|
||||
</permission>
|
||||
<permission name="extended-data" value="false"/>
|
||||
<permission name="execute-apps" value="true">
|
||||
<!-- default to "deny" or "allow" -->
|
||||
<application-list default="deny">
|
||||
<!-- type attr can be "deny" or "allow" nothing defaults to opposite of the list default so allow in this case -->
|
||||
<application name="info"/>
|
||||
<application name="hangup"/>
|
||||
</application-list>
|
||||
</permission>
|
||||
<permission name="expand-vars-in-tag-body" value="false">
|
||||
<!-- default to "deny" or "allow" -->
|
||||
<!-- type attr can be "deny" or "allow" nothing defaults to opposite of the list default so allow in this case -->
|
||||
<!--
|
||||
<variable-list default="deny">
|
||||
<variable name="caller_id_name"/>
|
||||
<variable name="hangup"/>
|
||||
</variable-list>
|
||||
|
||||
<api-list default="deny">
|
||||
<api name="expr"/>
|
||||
<api name="lua"/>
|
||||
</api-list>
|
||||
-->
|
||||
</permission>
|
||||
<permission name="dial" value="true"/>
|
||||
<permission name="dial-set-context" value="false"/>
|
||||
<permission name="dial-set-dialplan" value="false"/>
|
||||
<permission name="dial-set-cid-name" value="false"/>
|
||||
<permission name="dial-set-cid-number" value="false"/>
|
||||
<permission name="dial-full-originate" value="false"/>
|
||||
<permission name="conference" value="true"/>
|
||||
<permission name="conference-set-profile" value="false"/>
|
||||
</permissions>
|
||||
|
||||
<params>
|
||||
<!-- default url can be overridden by app data -->
|
||||
<param name="gateway-url" value="http://www.freeswitch.org/api/index.cgi" />
|
||||
|
||||
<!-- set this to provide authentication credentials to the server -->
|
||||
<!--<param name="gateway-credentials" value="muser:mypass"/>-->
|
||||
<!--<param name="auth-scheme" value="basic"/>-->
|
||||
|
||||
<!-- optional: this will enable the CA root certificate check by libcurl to
|
||||
verify that the certificate was issued by a major Certificate Authority.
|
||||
note: default value is disabled. only enable if you want this! -->
|
||||
<!--<param name="enable-cacert-check" value="true"/>-->
|
||||
<!-- optional: verify that the server is actually the one listed in the cert -->
|
||||
<!-- <param name="enable-ssl-verifyhost" value="true"/> -->
|
||||
|
||||
<!-- optional: these options can be used to specify custom SSL certificates
|
||||
to use for HTTPS communications. Either use both options or neither.
|
||||
Specify your public key with 'ssl-cert-path' and the private key with
|
||||
'ssl-key-path'. If your private key has a password, specify it with
|
||||
'ssl-key-password'. -->
|
||||
<!-- <param name="ssl-cert-path" value="$${base_dir}/conf/certs/public_key.pem"/> -->
|
||||
<!-- <param name="ssl-key-path" value="$${base_dir}/conf/certs/private_key.pem"/> -->
|
||||
<!-- <param name="ssl-key-password" value="MyPrivateKeyPassword"/> -->
|
||||
<!-- optional timeout -->
|
||||
<!-- <param name="timeout" value="10"/> -->
|
||||
|
||||
<!-- optional: use a custom CA certificate in PEM format to verify the peer
|
||||
with. This is useful if you are acting as your own certificate authority.
|
||||
note: only makes sense if used in combination with "enable-cacert-check." -->
|
||||
<!-- <param name="ssl-cacert-file" value="$${base_dir}/conf/certs/cacert.pem"/> -->
|
||||
|
||||
<!-- optional: specify the SSL version to force HTTPS to use. Valid options are
|
||||
"SSLv3" and "TLSv1". Otherwise libcurl will auto-negotiate the version. -->
|
||||
<!-- <param name="ssl-version" value="TLSv1"/> -->
|
||||
|
||||
<!-- optional: enables cookies and stores them in the specified file. -->
|
||||
<!-- <param name="cookie-file" value="/tmp/cookie-mod_xml_curl.txt"/> -->
|
||||
|
||||
<!-- one or more of these imply you want to pick the exact variables that are transmitted -->
|
||||
<!--<param name="enable-post-var" value="Caller-Unique-ID"/>-->
|
||||
</params>
|
||||
|
||||
</profile>
|
||||
</profiles>
|
||||
</configuration>
|
||||
@@ -1,10 +0,0 @@
|
||||
<configuration name="http_cache.conf" description="HTTP GET cache">
|
||||
<settings>
|
||||
<param name="max-urls" value="10000"/>
|
||||
<param name="location" value="$${base_dir}/http_cache"/>
|
||||
<param name="default-max-age" value="86400"/>
|
||||
<param name="prefetch-thread-count" value="8"/>
|
||||
<param name="prefetch-queue-size" value="100"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<configuration name="ivr.conf" description="IVR menus">
|
||||
<menus>
|
||||
<X-PRE-PROCESS cmd="include" data="../ivr_menus/*.xml"/>
|
||||
</menus>
|
||||
</configuration>
|
||||
@@ -1,8 +0,0 @@
|
||||
<configuration name="java.conf" description="Java Plug-Ins">
|
||||
<javavm path="/opt/jdk1.6.0_04/jre/lib/amd64/server/libjvm.so"/>
|
||||
<options>
|
||||
<option value="-Djava.class.path=$${base_dir}/scripts/freeswitch.jar:$${base_dir}/scripts/example.jar"/>
|
||||
<option value="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000"/>
|
||||
</options>
|
||||
<startup class="org/freeswitch/example/ApplicationLauncher" method="startup"/>
|
||||
</configuration>
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<!-- HTTP(S) logging -->
|
||||
<!-- URL where to POST JSON CDRs. Leave empty for no URL logging. Up to 20 URLs may be specified. -->
|
||||
<param name="url" value="http://127.0.0.1:2022/freeswitch_json"/>
|
||||
<param name="url" value="http://127.0.0.1:2080/freeswitch_json"/>
|
||||
<!-- Authentication scheme for the above URL. May be one of basic|digest|NTLM|GSS-NEGOTIATE|any-->
|
||||
<param name="auth-scheme" value="basic"/>
|
||||
<!-- Credentials in the form usernameassword if auth-scheme is used. Leave empty for no authentication. -->
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<configuration name="lcr.conf" description="LCR Configuration">
|
||||
<settings>
|
||||
<param name="odbc-dsn" value="freeswitch-mysql:freeswitch:Fr33Sw1tch"/>
|
||||
<!-- <param name="odbc-dsn" value="freeswitch-pgsql:freeswitch:Fr33Sw1tch"/> -->
|
||||
</settings>
|
||||
<profiles>
|
||||
<profile name="default">
|
||||
<param name="id" value="0"/>
|
||||
<param name="order_by" value="rate,quality,reliability"/>
|
||||
</profile>
|
||||
<profile name="qual_rel">
|
||||
<param name="id" value="1"/>
|
||||
<param name="order_by" value="quality,reliability"/>
|
||||
</profile>
|
||||
<profile name="rel_qual">
|
||||
<param name="id" value="2"/>
|
||||
<param name="order_by" value="reliability,quality"/>
|
||||
</profile>
|
||||
<!--
|
||||
Some samples of how to do custom SQL:
|
||||
|
||||
=============================================================
|
||||
PostgreSQL with contrib prefix module which supports fast
|
||||
prefix queries. Ideal option.
|
||||
=============================================================
|
||||
<profile name="pg_prefix">
|
||||
<param name="custom_sql" value="
|
||||
SELECT l.digits AS lcr_digits,
|
||||
c.carrier_name AS lcr_carrier_name,
|
||||
l.${lcr_rate_field} as lcr_rate_field,
|
||||
cg.prefix AS lcr_gw_prefix, cg.suffix AS lcr_gw_suffix,
|
||||
l.lead_strip AS lcr_lead_strip, l.trail_strip AS lcr_trail_strip,
|
||||
l.prefix AS lcr_prefix, l.suffix AS lcr_suffix
|
||||
FROM lcr l
|
||||
JOIN carriers c ON l.carrier_id=c.id
|
||||
JOIN carrier_gateway cg ON c.id=cg.carrier_id
|
||||
WHERE c.enabled = '1' AND cg.enabled = '1' AND l.enabled = '1'
|
||||
AND digits_prefix @> %q
|
||||
AND CURRENT_TIMESTAMP BETWEEN date_start AND date_end
|
||||
ORDER BY digits DESC, ${lcr_rate_field}, random();
|
||||
"/>
|
||||
</profile>
|
||||
|
||||
=============================================================
|
||||
PostgreSQL with contrib prefix module which supports fast
|
||||
prefix queries. Ideal option. Alternate syntax which requies
|
||||
a session but allows variable substitution.
|
||||
=============================================================
|
||||
<profile name="pg_prefix2">
|
||||
<param name="custom_sql" value="
|
||||
SELECT l.digits AS lcr_digits,
|
||||
c.carrier_name AS lcr_carrier_name,
|
||||
l.${lcr_rate_field} as lcr_rate_field,
|
||||
cg.prefix AS lcr_gw_prefix, cg.suffix AS lcr_gw_suffix,
|
||||
l.lead_strip AS lcr_lead_strip, l.trail_strip AS lcr_trail_strip,
|
||||
l.prefix AS lcr_prefix, l.suffix AS lcr_suffix
|
||||
FROM lcr l
|
||||
JOIN carriers c ON l.carrier_id=c.id
|
||||
JOIN carrier_gateway cg ON c.id=cg.carrier_id
|
||||
WHERE c.enabled = '1' AND cg.enabled = '1' AND l.enabled = '1'
|
||||
AND digits_prefix @> '${lcr_query_digits}'
|
||||
AND CURRENT_TIMESTAMP BETWEEN date_start AND date_end
|
||||
ORDER BY digits DESC, ${lcr_rate_field}, random();
|
||||
"/>
|
||||
</profile>
|
||||
|
||||
=============================================================
|
||||
Demonstrates use of computed inlist.
|
||||
=============================================================
|
||||
<profile name="inlist">
|
||||
<param name="custom_sql" value="
|
||||
SELECT l.digits AS lcr_digits,
|
||||
c.carrier_name AS lcr_carrier_name,
|
||||
l.${lcr_rate_field} as lcr_rate_field,
|
||||
cg.prefix AS lcr_gw_prefix, cg.suffix AS lcr_gw_suffix,
|
||||
l.lead_strip AS lcr_lead_strip, l.trail_strip AS lcr_trail_strip,
|
||||
l.prefix AS lcr_prefix, l.suffix AS lcr_suffix
|
||||
FROM lcr l
|
||||
JOIN carriers c ON l.carrier_id=c.id
|
||||
JOIN carrier_gateway cg ON c.id=cg.carrier_id
|
||||
WHERE c.enabled = '1' AND cg.enabled = '1' AND l.enabled = '1'
|
||||
AND digits IN (${lcr_query_expanded_digits})
|
||||
AND CURRENT_TIMESTAMP BETWEEN date_start AND date_end
|
||||
ORDER BY digits DESC, ${lcr_rate_field}, random();
|
||||
"/>
|
||||
</profile>
|
||||
-->
|
||||
</profiles>
|
||||
</configuration>
|
||||
@@ -1,49 +0,0 @@
|
||||
<configuration name="local_stream.conf" description="stream files from local dir">
|
||||
<!-- fallback to default if requested moh class isn't found -->
|
||||
<directory name="default" path="$${sounds_dir}/music/8000">
|
||||
<param name="rate" value="8000"/>
|
||||
<param name="shuffle" value="true"/>
|
||||
<param name="channels" value="1"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="timer-name" value="soft"/>
|
||||
<!-- list of short files to break in with every so often -->
|
||||
<!--<param name="chime-list" value="file1.wav,file2.wav"/>-->
|
||||
<!-- frequency of break-in (seconds)-->
|
||||
<!--<param name="chime-freq" value="30"/>-->
|
||||
<!-- limit to how many seconds the file will play -->
|
||||
<!--<param name="chime-max" value="500"/>-->
|
||||
</directory>
|
||||
|
||||
<directory name="moh/8000" path="$${sounds_dir}/music/8000">
|
||||
<param name="rate" value="8000"/>
|
||||
<param name="shuffle" value="true"/>
|
||||
<param name="channels" value="1"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="timer-name" value="soft"/>
|
||||
</directory>
|
||||
|
||||
<directory name="moh/16000" path="$${sounds_dir}/music/16000">
|
||||
<param name="rate" value="16000"/>
|
||||
<param name="shuffle" value="true"/>
|
||||
<param name="channels" value="1"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="timer-name" value="soft"/>
|
||||
</directory>
|
||||
|
||||
<directory name="moh/32000" path="$${sounds_dir}/music/32000">
|
||||
<param name="rate" value="32000"/>
|
||||
<param name="shuffle" value="true"/>
|
||||
<param name="channels" value="1"/>
|
||||
<param name="interval" value="20"/>
|
||||
<param name="timer-name" value="soft"/>
|
||||
</directory>
|
||||
<!--
|
||||
<directory name="moh/48000" path="$${sounds_dir}/music/48000">
|
||||
<param name="rate" value="48000"/>
|
||||
<param name="shuffle" value="true"/>
|
||||
<param name="channels" value="1"/>
|
||||
<param name="interval" value="10"/>
|
||||
<param name="timer-name" value="soft"/>
|
||||
</directory>
|
||||
-->
|
||||
</configuration>
|
||||
@@ -1,29 +0,0 @@
|
||||
<configuration name="logfile.conf" description="File Logging">
|
||||
<settings>
|
||||
<!-- true to auto rotate on HUP, false to open/close -->
|
||||
<param name="rotate-on-hup" value="true"/>
|
||||
</settings>
|
||||
<profiles>
|
||||
<profile name="default">
|
||||
<settings>
|
||||
<!-- File to log to -->
|
||||
<!--<param name="logfile" value="/var/log/freeswitch.log"/>-->
|
||||
<!-- At this length in bytes rotate the log file (0 for never) -->
|
||||
<param name="rollover" value="10485760"/>
|
||||
<!-- Maximum number of log files to keep before wrapping -->
|
||||
<!-- If this parameter is enabled, the log filenames will not include a date stamp -->
|
||||
<!-- <param name="maximum-rotate" value="32"/> -->
|
||||
<!-- Uncomment to prefix all log lines by the session's uuid -->
|
||||
<!-- <param name="uuid" value="true" /> -->
|
||||
</settings>
|
||||
<mappings>
|
||||
<!--
|
||||
name can be a file name, function name or 'all'
|
||||
value is one or more of debug,info,notice,warning,err,crit,alert,all
|
||||
Please see comments in console.conf.xml for more information
|
||||
-->
|
||||
<map name="all" value="debug,info,notice,warning,err,crit,alert"/>
|
||||
</mappings>
|
||||
</profile>
|
||||
</profiles>
|
||||
</configuration>
|
||||
@@ -1,30 +0,0 @@
|
||||
<configuration name="lua.conf" description="LUA Configuration">
|
||||
<settings>
|
||||
|
||||
<!--
|
||||
Specify local directories that will be searched for LUA modules
|
||||
These entries will be pre-pended to the LUA_CPATH environment variable
|
||||
-->
|
||||
<!-- <param name="module-directory" value="/usr/lib/lua/5.1/?.so"/> -->
|
||||
<!-- <param name="module-directory" value="/usr/local/lib/lua/5.1/?.so"/> -->
|
||||
|
||||
<!--
|
||||
Specify local directories that will be searched for LUA scripts
|
||||
These entries will be pre-pended to the LUA_PATH environment variable
|
||||
-->
|
||||
<!-- <param name="script-directory" value="/usr/local/lua/?.lua"/> -->
|
||||
<!-- <param name="script-directory" value="$${base_dir}/scripts/?.lua"/> -->
|
||||
|
||||
<!--<param name="xml-handler-script" value="/dp.lua"/>-->
|
||||
<!--<param name="xml-handler-bindings" value="dialplan"/>-->
|
||||
|
||||
<!--
|
||||
The following options identifies a lua script that is launched
|
||||
at startup and may live forever in the background.
|
||||
You can define multiple lines, one for each script you
|
||||
need to run.
|
||||
-->
|
||||
<!--<param name="startup-script" value="startup_script_1.lua"/>-->
|
||||
<!--<param name="startup-script" value="startup_script_2.lua"/>-->
|
||||
</settings>
|
||||
</configuration>
|
||||
@@ -1,6 +0,0 @@
|
||||
<configuration name="memcache.conf" description="memcache Configuration">
|
||||
<settings>
|
||||
<!-- comma sep list of servers: eg: localhost,otherhost:port,anotherone -->
|
||||
<param name="memcache-servers" value="localhost"/>
|
||||
</settings>
|
||||
</configuration>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user