273 Commits

Author SHA1 Message Date
DanB
24fda5b14b Changelog update 2014-03-25 17:25:15 +01:00
DanB
f3f6bb1e16 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-25 17:19:46 +01:00
DanB
9211c01d69 Fixup tutorial readmes 2014-03-25 17:18:09 +01:00
Radu Ioan Fericean
87481ed520 rename ActionTimings Tag and Id 2014-03-25 18:17:17 +02:00
DanB
5857e63823 Changing documentation index to rc4 2014-03-25 17:07:50 +01:00
DanB
9ebf2573b0 Adding console command: rem_cdrs 2014-03-25 17:05:00 +01:00
DanB
e67db4a434 Tests fixup for exporter 2014-03-25 16:42:45 +01:00
DanB
375bf8c0dd CdrExporter returns stats for successfuly and unsuccessfuly exported CDRs 2014-03-25 16:00:33 +01:00
DanB
6acfa22a04 Adding localtests for RemStoredCdrs 2014-03-25 13:40:06 +01:00
DanB
29e3bd137b Fixup build, removed removeFromDb parameter in cdr_export console command 2014-03-25 13:30:39 +01:00
DanB
f390281f41 API for CdrExporter fixed_width, RemCdr separated from exporter due to buggy scenario discovered 2014-03-24 22:23:03 +01:00
DanB
881db9c1c4 Adding required to fixed_width filter, adding export of the header and trailer, tests 2014-03-24 16:50:57 +01:00
DanB
def252d153 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-23 21:40:17 +01:00
DanB
2bc4e3fb7e Basic implementation of fixed_width cdr export content 2014-03-23 21:17:48 +01:00
Radu Ioan Fericean
e381265ace save dirty balances at the end of debit 2014-03-23 20:21:05 +02:00
DanB
15f2a2cd82 Adding more properties to XmlCdrFields config 2014-03-23 09:14:43 +01:00
DanB
4bb3e745f1 Adding config xml template for exporter with fixed width CDRs together with tests 2014-03-22 22:16:59 +01:00
DanB
4045d022ae Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-22 16:51:03 +01:00
DanB
5e06cafd2f Adding rating aliases, account aliases, shared groups to cache stats struct, improving fs_json tutorial tests and documentation 2014-03-22 16:50:25 +01:00
DanB
a93ecfc049 Adding initial xmlconfig structure with tests 2014-03-22 16:49:06 +01:00
Radu Ioan Fericean
5d0e6b4fee removed superflous if 2014-03-21 18:15:46 +02:00
Radu Ioan Fericean
5de1a83bf5 process all balances on debit (own+shared) 2014-03-21 18:01:28 +02:00
Radu Ioan Fericean
c6de153685 more tests and disabled shared gropup caching 2014-03-20 19:05:31 +02:00
Radu Ioan Fericean
22adfb552f debit from shared group empty balances 2014-03-20 17:27:29 +02:00
Radu Ioan Fericean
b0cd66509c fix for shared get max session 2014-03-20 12:53:36 +02:00
DanB
4d02ede7df Shared balance local tests improvements 2014-03-20 10:39:04 +01:00
DanB
d77c5463ac Adding local test for MaxSessionTime out of shared balance 2014-03-19 21:57:23 +01:00
Radu Ioan Fericean
19b7d0beb7 max session caclulus to consider shared groups 2014-03-19 19:09:25 +02:00
Radu Ioan Fericean
315eddb63f moved locking before debit 2014-03-19 17:32:02 +02:00
Radu Ioan Fericean
acda24e46e ansible variables 2014-03-19 15:06:46 +02:00
Radu Ioan Fericean
af16503069 added profile variables to ansible 2014-03-19 13:06:51 +02:00
Radu Ioan Fericean
822a921a37 more ansible provision commands 2014-03-18 19:41:06 +02:00
DanB
6017827059 Small tweaks to make local tests running with vagrant 2014-03-18 14:55:46 +00:00
DanB
3892d9ed20 LoadTariffPlanFromFolder, tutfscsv tests fixup 2014-03-18 14:09:09 +01:00
Radu Ioan Fericean
5322063103 use *mine_random as default strategy 2014-03-18 11:09:49 +02:00
DanB
e8393b9bcc Modified sample tariffplan for fs_json tutorial 2014-03-18 08:43:07 +01:00
Radu Ioan Fericean
724ebdc039 split rating profile and account aliases 2014-03-17 21:28:25 +02:00
DanB
2dbc80166e Adding sharedGroup loading inside csv and db loaders, tariff plans for fs_json tutorial 2014-03-17 19:10:21 +01:00
DanB
ccac8236f4 Fix test localtest for new rating IdCC 2014-03-17 17:55:46 +01:00
DanB
3c34310ab2 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-17 17:46:57 +01:00
DanB
2ef0f4f492 Adding regexp rules for shared groups csv file 2014-03-17 17:46:53 +01:00
Radu Ioan Fericean
f6741c6d88 Added rating id and destination id to call cost 2014-03-17 18:37:50 +02:00
DanB
62e9d02239 Fix regexp rules to accept aliases 2014-03-17 16:18:52 +01:00
DanB
2e80f8a1f4 Fixup sharedGroup number of columns 2014-03-17 15:58:33 +01:00
DanB
3aecec9bb8 Renaming cdrexporter package into cdre 2014-03-17 10:00:00 +01:00
DanB
e2e3b8d2f2 Local tests for GetCallCostLog API, LogCallCost and GetCallCostLog storage methods 2014-03-16 21:01:27 +01:00
DanB
86317e4b19 Adding apier/cdrs.go file 2014-03-16 19:55:52 +01:00
DanB
f2f46f8054 Adding GetCallCostLog API and console command 2014-03-16 19:51:30 +01:00
DanB
c66f4f2710 Local tests for SetCdr, SetRatedCdr and GetStoredCdrs 2014-03-16 18:49:05 +01:00
DanB
ee31976401 Adding detailed CDR export filters both in storage and APIs 2014-03-16 16:21:18 +01:00
DanB
f6d16cecc5 Adding full CDR template in exported CDRs, using RSRFields 2014-03-16 13:11:08 +01:00
DanB
db433a760f Adding search/replace regexp rules for FreeSWITCH extra fields 2014-03-15 19:17:40 +01:00
DanB
47700a924f Adding RSRField and tests around 2014-03-15 18:48:30 +01:00
DanB
548a4ea64f Adding ReSearchReplace struct 2014-03-15 16:30:17 +01:00
DanB
3227434fd9 Adding tests for search&replace functions 2014-03-15 14:19:04 +01:00
DanB
c344554bbb Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-14 21:14:10 +01:00
DanB
57be22dd87 Adding search and replace regexp utils 2014-03-14 21:14:05 +01:00
Radu Ioan Fericean
a3cc1fedfd use unixnano for random 2014-03-14 19:53:47 +02:00
Radu Ioan Fericean
6685b7b444 add code comment for fscdr body 2014-03-14 19:31:52 +02:00
Radu Ioan Fericean
10b27fa978 better test for random shared balance 2014-03-14 19:30:55 +02:00
Radu Ioan Fericean
e2a50a77fd search whole cdr if extra field not found in variables 2014-03-14 19:21:56 +02:00
DanB
00b47c1367 More local tests for tutorials 2014-03-13 19:50:49 +01:00
DanB
cdd83ea531 Adding local tests for tutfsjson 2014-03-13 18:40:32 +01:00
DanB
d33299f0ab Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-13 17:36:04 +01:00
DanB
5083c7479d Adding re-cache in case of LoadRatingProfile also 2014-03-13 17:35:49 +01:00
DanB
37b21d2bb5 Fixup re-cache in case of SetRatingProfile api 2014-03-13 17:01:02 +01:00
DanB
e09cc8527e Empty call costs in session should not be saved since they break 2014-03-12 19:00:21 +01:00
Radu Ioan Fericean
0855d09fea shared group strategy fully customizable 2014-03-11 18:15:12 +02:00
DanB
c8553e9611 Add 1007 as prepaid account in fs_json tutorial to test shared balances 2014-03-10 11:19:17 +01:00
DanB
d8fb33aee3 Fix cdrs port in fs_json tutorial, add 1006 as prepaid account to test shared balances 2014-03-10 11:17:45 +01:00
DanB
3f30cdb7ed Concurrency fixup on saveOperations in emulation mode 2014-03-08 18:34:22 +01:00
DanB
b9681f2d1c Adding checkConfigSanity integrated in config type 2014-03-08 17:37:02 +01:00
DanB
d6a0825142 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-08 16:22:43 +01:00
DanB
8d61099de1 SessionManager - Multiple sessions emulated out of one request to support scenarios like reseller chains 2014-03-08 16:22:22 +01:00
Radu Ioan Fericean
6ba20aee03 smal fix for csv header 2014-03-07 12:16:27 +02:00
DanB
b28a02512c Adding runids in session manager config 2014-03-06 19:49:24 +01:00
DanB
5ae7a18283 Adding setupTime to CDRs for later stats calculation 2014-03-06 15:47:39 +01:00
DanB
5b10f63c94 Reload cache on SetRatingProfile API 2014-03-05 19:10:27 +01:00
DanB
90e3791c0d Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-05 18:34:35 +01:00
Radu Ioan Fericean
fdeecafe37 make sure the aliases are up-to-date after load 2014-03-05 16:54:29 +02:00
Radu Ioan Fericean
71a50dc78e duplicate check for db loader too 2014-03-05 14:56:54 +02:00
Radu Ioan Fericean
8f47264248 check for duplicate account actions 2014-03-05 14:51:18 +02:00
Radu Ioan Fericean
a753a55119 fixed connectFee in max debit issue 2014-03-05 14:17:47 +02:00
DanB
2556fb5e69 Fix in documentation changed default CDRs port which is now 2080 2014-03-05 12:06:09 +01:00
Radu Ioan Fericean
0b0474fa23 fix for MaxDebit bug
max duration was calculated badly
2014-03-04 22:02:20 +02:00
Radu Ioan Fericean
8967c2fa3a fix rounding up problem 2014-03-03 20:37:13 +02:00
DanB
bea8434d99 Adding apier_local_test.cfg file 2014-03-02 18:15:22 +01:00
DanB
bd5b2607e8 More local tests changes to write in /tmp 2014-03-02 18:09:39 +01:00
DanB
62ccb5b9df Automatic starting daemons out of sample configs in localtests, /tmp as output folder to work in non-root mode 2014-03-02 17:34:03 +01:00
DanB
755ab02d55 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-02 12:09:15 +01:00
DanB
cf0179e505 Adding FS_SIP_REQUSER as fallback variable for destination in case of fscdr 2014-03-02 12:09:09 +01:00
Radu Ioan Fericean
bde488c46b ignore vagrant ansible host file 2014-03-02 12:56:14 +02:00
Radu Ioan Fericean
68aefe4912 removed vagrant generated file 2014-03-02 12:56:14 +02:00
DanB
487a1c5da6 Fix tariffplan data and sleep in tutfscsv_local_test 2014-03-02 09:43:27 +01:00
DanB
64711c6b87 Sleep in local tests to allow topup to happen 2014-03-02 09:29:41 +01:00
DanB
781db890d1 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-03-01 10:29:23 +01:00
DanB
7b618f8806 Preparing packaging for rc4 2014-03-01 10:29:16 +01:00
Radu Ioan Fericean
9006c5ed18 removed logs from tests 2014-03-01 00:11:05 +02:00
Radu Ioan Fericean
e6b02b84c0 extra tests for topup after load 2014-03-01 00:02:56 +02:00
Radu Ioan Fericean
b557e2a5a4 fixed connect fee issues 2014-02-28 23:24:55 +02:00
Radu Ioan Fericean
9a06d5e537 fixes for ansible devel 2014-02-28 18:40:39 +02:00
Radu Ioan Fericean
be8ea3bcc9 fixed devel ansible 2014-02-28 18:40:39 +02:00
Radu Ioan Fericean
4833eb94c3 splitted in devel/release 2014-02-28 18:40:39 +02:00
Radu Ioan Fericean
f717bb9542 changed travis rules and removed stats 2014-02-28 18:40:39 +02:00
DanB
2646e8fa6e Local tests to capture toup 2014-02-28 10:46:28 +01:00
DanB
ed313c3222 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-02-28 10:46:02 +01:00
Radu Ioan Fericean
25b15b8f0f map storage has compression too
only needed for better emulating redis
2014-02-28 11:08:03 +02:00
Radu Ioan Fericean
7d9326965d don't overwrite account on load 2014-02-28 10:47:30 +02:00
DanB
52ae44fc34 Fixup sample tariffplans 2014-02-27 17:48:45 +01:00
DanB
a0aa0cc05d Merge branch 'master' of https://github.com/cgrates/cgrates 2014-02-27 17:38:49 +01:00
DanB
b9d52c6401 Fix validation for Actions, now with shared balances 2014-02-27 17:38:42 +01:00
Radu Ioan Fericean
48d1720430 refund credit to shared group accounts 2014-02-27 17:12:59 +02:00
Radu Ioan Fericean
39e93e7fb8 fixses and tests for shared balances 2014-02-27 16:54:11 +02:00
Radu Ioan Fericean
49a3df285b test for shared group members loading 2014-02-27 15:30:29 +02:00
Radu Ioan Fericean
fc91e35433 shared balance fix 2014-02-27 14:04:59 +02:00
Radu Ioan Fericean
95d4af8ab7 more rounding and maxxsession improvement 2014-02-27 12:23:11 +02:00
Radu Ioan Fericean
83dd4efeab more aliases tests 2014-02-27 09:34:48 +02:00
Radu Ioan Fericean
b3dee97bc4 fixses and tests for aliases 2014-02-26 20:17:59 +02:00
Radu Ioan Fericean
074313b0f8 first aliases implementation
tests pending
2014-02-26 18:29:49 +02:00
Radu Ioan Fericean
8d98436656 rounding after debit 2014-02-26 11:14:13 +02:00
Radu Ioan Fericean
477af9467f added load test for shared groups 2014-02-26 11:14:13 +02:00
DanB
1a8e7f4631 Testing tutfscsv including maxDebit and unitsCounter for general usage 2014-02-25 20:52:11 +01:00
DanB
670b748b01 Renaming tag->id and old id->tbid in mysql tables 2014-02-25 13:53:13 +01:00
DanB
ddbd97ad20 Fix merge in tutfscsv_local 2014-02-24 12:47:23 +01:00
DanB
fe4506008f Local test TutFsCsv from config file 2014-02-24 12:41:24 +01:00
Radu Ioan Fericean
0aa5223f78 Merge branch 'master' into shared_balances
Conflicts:
	engine/account.go
	engine/loader_csv_test.go
	engine/storage_interface.go
	engine/storage_map.go
	engine/storage_redis.go
2014-02-21 16:36:48 +02:00
Radu Ioan Fericean
a37dbf0734 renamed Account Type attribute
It's now AllowNegative and it's a boolean (was *prepaid/*postpaid)
2014-02-21 15:52:47 +02:00
Radu Ioan Fericean
7831a53797 renamed USerBalance to Account 2014-02-21 14:37:31 +02:00
Radu Ioan Fericean
f701a42948 renamed action's BalanceId to BalanceType
also added a topup test
2014-02-21 13:14:06 +02:00
Radu Ioan Fericean
164b9f8945 fixed database credentials 2014-02-20 23:12:53 +02:00
Radu Ioan Fericean
a3daecdbc9 first running vagrant-ansible version 2014-02-20 22:13:43 +02:00
Radu Ioan Fericean
a91873800d started ansible vagrant box 2014-02-20 22:00:05 +02:00
Radu Ioan Fericean
f463dd2755 added *daily and *yearly for balance expiration time 2014-02-20 18:07:10 +02:00
Radu Ioan Fericean
f052b14214 added some comments 2014-02-20 17:29:13 +02:00
Radu Ioan Fericean
5f1923d694 Merge branch 'master' into shared_balances 2014-02-18 15:38:03 +02:00
Radu Ioan Fericean
c52b161b73 allow seetting rate subject and expiration with AddBalance api 2014-02-18 15:37:03 +02:00
DanB
d7a0446585 Fixup localtests 2014-02-18 11:06:09 +01:00
DanB
216ae2b767 Fixup install documentation to reflect new repo path 2014-02-18 09:27:28 +01:00
DanB
e60fc8cb96 Capturing the output of the engine in tests 2014-02-18 09:25:10 +01:00
Radu Ioan Fericean
8008c76154 Merge branch 'master' into shared_balances 2014-02-17 18:05:54 +02:00
DanB
d929e14159 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-02-17 16:25:41 +01:00
DanB
dd2eb2f97a session_manager/event interface with field filter, preparing for multiple sessions 2014-02-17 16:25:22 +01:00
Radu Ioan Fericean
e0476a3130 Add members on balance creation 2014-02-17 14:50:35 +02:00
Radu Ioan Fericean
fdc451244a fixed typo 2014-02-17 01:28:50 +02:00
Radu Ioan Fericean
098a1f6d3f fixed account actions ids 2014-02-17 01:22:55 +02:00
Radu Ioan Fericean
75046b1f36 more shared group fixes 2014-02-17 01:16:31 +02:00
Radu Ioan Fericean
dec4ebf2f1 added distinct field parameter 2014-02-17 01:09:37 +02:00
Radu Ioan Fericean
4154a3beef Merge ssh://192.168.0.9/home/rif/Documents/prog/go/src/github.com/cgrates/cgrates into shared_balances 2014-02-17 00:43:04 +02:00
Radu Ioan Fericean
0efb2f1094 load shared groups from csv 2014-02-17 00:42:41 +02:00
Radu Ioan Fericean
5371688158 Merge ssh://192.168.0.9/home/rif/Documents/prog/go/src/github.com/cgrates/cgrates into shared_balances 2014-02-17 00:40:57 +02:00
Radu Ioan Fericean
51ee4091bc shared group fixes 2014-02-17 00:39:15 +02:00
Radu Ioan Fericean
540412033f used local addres in tests 2014-02-16 23:13:27 +02:00
Radu Ioan Fericean
a731a33238 Merge branch 'master' into shared_balances 2014-02-16 22:52:20 +02:00
Radu Ioan Fericean
96092faffc Moved build status up 2014-02-13 13:50:41 +02:00
Radu Ioan Fericean
beea40977c Fixed bad copy paste 2014-02-13 13:24:31 +02:00
Radu Ioan Fericean
ffaeac112c Update README.md 2014-02-13 13:23:27 +02:00
Radu Ioan Fericean
9c46b61c73 Api links point to apier package 2014-02-13 13:17:00 +02:00
Radu Ioan Fericean
1ab852e3cd Update README.md 2014-02-13 13:14:51 +02:00
Radu Ioan Fericean
4e956eaa8c Update README.md 2014-02-13 13:14:38 +02:00
DanB
a5f5274095 Adding config for additional sessions management 2014-02-13 11:55:10 +01:00
DanB
4c894ee9de Adding original apier_local_test 2014-02-13 10:13:12 +01:00
Radu Ioan Fericean
361d75d8cf compressed all GetTP*Ids methods into one 2014-02-13 00:01:58 +02:00
Radu Ioan Fericean
64e79764de build fix 2014-02-12 16:36:56 +02:00
Radu Ioan Fericean
6ad5794bfc added db loader for shared balances 2014-02-12 16:23:03 +02:00
Radu Ioan Fericean
c447fa49dc Merge branch 'master' into shared_balances 2014-02-12 14:18:03 +02:00
DanB
9042c98492 Fixup mediation with rerating 2014-02-11 12:57:00 +01:00
DanB
bd71dc9a4c Test fixups 2014-02-10 20:00:30 +01:00
DanB
944262ccff Adding local_test for mediator rpc method 2014-02-10 18:31:27 +01:00
Radu Ioan Fericean
fc1e7aed5a merged master 2014-02-10 15:12:01 +02:00
DanB
b91fc4ca21 Adding mediator_rpc 2014-02-10 14:08:24 +01:00
Radu Ioan Fericean
b784edf5aa Merge branch 'master' into shared_balances
Conflicts:
	engine/balances.go
	utils/consts.go
2014-02-07 22:32:03 +02:00
Radu Ioan Fericean
914fe77117 rating subject can have multiple predefined values
Valid values can start with *zero + any duration 1m, 15m, 1h10m
2014-02-07 22:26:39 +02:00
Radu Ioan Fericean
77ea2754cb GetMaxSessionDuration fix
should return no more than initial call descriptor duration
2014-02-07 19:54:09 +02:00
Radu Ioan Fericean
cd531849d1 Merge branch 'master' into shared_balances 2014-02-07 19:12:28 +02:00
Radu Ioan Fericean
dadf06f3ef get max session duration method was modifing call descriptor 2014-02-07 19:10:44 +02:00
Radu Ioan Fericean
9e88719e05 fixed build problem 2014-02-07 12:24:52 +02:00
Radu Ioan Fericean
3a3b92e9d6 little cleanup 2014-02-07 11:13:39 +02:00
Radu Ioan Fericean
a9d698e029 Merge branch 'master' into shared_balances
Conflicts:
	engine/balances.go
	engine/userbalance.go
2014-02-07 10:57:29 +02:00
Radu Ioan Fericean
aa99a1e526 make sure connect fee is deducted only on LoopIndex = 0 2014-02-06 19:36:42 +02:00
Radu Ioan Fericean
f154dac933 connect fee changes
The connect fee is take from the first timespan after balance debit.
So if the balance has ratingsubject than that connect fee will be applied.
2014-02-06 18:56:21 +02:00
Radu Ioan Fericean
59f650e3f8 small addition last night fix 2014-02-06 10:15:27 +02:00
Radu Ioan Fericean
462152a76d fixed rating with rating subject bug 2014-02-06 01:34:38 +02:00
Radu Ioan Fericean
3bf3f04cc6 removed connect fee as a separte field in call cost 2014-02-06 01:34:38 +02:00
DanB
9f9174d0cc Console get_maxduration command implementation 2014-02-05 18:51:15 +01:00
DanB
e6ef9f8155 DebitBalance console command, fix apier local_test for GetUserBalances 2014-02-05 17:59:49 +01:00
Radu Ioan Fericean
bea20cf98d fixed history files creation 2014-02-05 14:08:49 +02:00
Radu Ioan Fericean
59e6e945b5 small typo fix 2014-02-05 14:08:37 +02:00
Radu Ioan Fericean
77c326cccf action rating subject may or may not start with a * 2014-02-04 20:54:48 +02:00
Radu Ioan Fericean
709594ffa9 get_balance console renamed to get_balances
returns json formatted userbalance object
2014-02-04 15:28:02 +02:00
Radu Ioan Fericean
58d485f46f small console fixes 2014-02-04 14:40:58 +02:00
DanB
4cbadb4158 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-02-03 19:04:53 +01:00
DanB
caba3af732 Mediation RPC with rerate abilitites, rename RatedCDR -> StoredCdr, adding storage interface tests 2014-02-03 19:04:42 +01:00
Radu Ioan Fericean
727337e617 start shared groups cvs loading
modified shared group structure
2014-02-03 18:50:26 +02:00
Radu Ioan Fericean
20a0ae4449 removed direction and added destinationid to add_balance console command
th consoled is aimed to be used for simple and quick test actions
2014-02-03 17:34:37 +02:00
Radu Ioan Fericean
5f331f3161 add direction parameter for add_balance console command 2014-02-03 15:13:22 +02:00
Radu Ioan Fericean
c7ea2cd263 miminmum match length is now parametrized and can be moved in the confs
Note that it affects the general speed
2014-02-03 10:53:32 +02:00
Radu Ioan Fericean
30a842a6b9 miminmum match length is now parametrized and can be moved in the confs
Note that it affects the general speed
2014-02-01 14:04:12 +02:00
DanB
3f7bc14b44 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-01-31 19:31:15 +01:00
Radu Ioan Fericean
6fcd794005 moved hist init before populate 2014-01-31 18:49:49 +02:00
Radu Ioan Fericean
750260505c build fix 2014-01-31 17:22:32 +02:00
Radu Ioan Fericean
5c2d1a9c1a Merge branch 'master' into shared_balances 2014-01-31 17:08:44 +02:00
Radu Ioan Fericean
d90e67b966 moved get next start date from shared balances branch 2014-01-31 17:07:36 +02:00
Radu Ioan Fericean
0b7f315d49 match one symbol prefix as well 2014-01-31 17:05:05 +02:00
Radu Ioan Fericean
a239b8d760 bigger refactoring for next start date calculus 2014-01-31 16:37:48 +02:00
Radu Ioan Fericean
0490eaef8c split shared debiting methods minutes/money 2014-01-29 21:54:17 +02:00
Radu Ioan Fericean
ea6c8371d2 Merge branch 'master' into shared_balances 2014-01-29 21:45:05 +02:00
Radu Ioan Fericean
4032274748 fixed next start time test 2014-01-29 18:39:36 +02:00
DanB
edf5007f9e Session manager following api changes in FSock 2014-01-29 11:02:37 +01:00
Radu Ioan Fericean
31eebb7e81 Merge branch 'master' into shared_balances 2014-01-28 13:00:40 +02:00
Radu Ioan Fericean
5d190f0f2a add actions for enabling/disabling user 2014-01-28 13:00:04 +02:00
Radu Ioan Fericean
a5fad89574 added disabling facility for user accounts 2014-01-28 12:22:50 +02:00
Radu Ioan Fericean
23a676da95 got engine from master 2014-01-27 11:46:59 +02:00
Radu Ioan Fericean
69cf1cc896 Merge branch 'master' into shared_balances
Conflicts:
	apier/v1/apier_local_test.go
	apier/v1/tutfscsv_local_test.go
	cmd/cgr-engine/cgr-engine.go
	engine/storage_map.go
	engine/storage_mongo.go
	engine/storage_redis.go
2014-01-27 11:18:25 +02:00
Radu Ioan Fericean
b735688fe5 Remaned to hasdata method 2014-01-27 11:04:25 +02:00
DanB
6a251c2b2c Tutorials config updates for rc4 2014-01-26 10:33:02 +01:00
DanB
21b4c10836 Tests - cdrc/RecordAsRatedCdr ratedcdr/AsRawCdrHttpForm 2014-01-26 09:45:51 +01:00
DanB
1bbe5a0a88 CDRc communicating over internal interface with the CDRs 2014-01-25 20:52:32 +01:00
DanB
8733b5219e Mediator and SessionManager wait for cache to come up before starting over internal interface 2014-01-25 14:28:03 +01:00
DanB
f61aca8d91 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-01-25 14:03:18 +01:00
Radu Ioan Fericean
3bbb67c72f working variant for history server and gob enc 2014-01-25 11:52:11 +02:00
Radu Ioan Fericean
8177e64f8b new histtory tests 2014-01-25 11:52:11 +02:00
DanB
9210b20924 Fmt on sources :( 2014-01-25 10:49:00 +01:00
DanB
5b424d0e70 Engine components sync via chans 2014-01-25 10:46:21 +01:00
Radu Ioan Fericean
b6dde967f2 samll engine fix 2014-01-24 17:35:57 +02:00
Radu Ioan Fericean
c50c202fe6 Merge branch 'master' into shared_balances
Conflicts:
	apier/v1/apier_local_test.go
	apier/v1/tutfscsv_local_test.go
	cmd/cgr-engine/cgr-engine.go
2014-01-24 10:26:55 +02:00
Radu Ioan Fericean
5f9d18fe0f removed extra log 2014-01-24 09:37:30 +02:00
Radu Ioan Fericean
a31231de09 merge 2014-01-24 09:33:32 +02:00
DanB
ddeb13bcc4 Engine start order, improved logging, default config listening fix 2014-01-23 18:19:03 +01:00
Radu Ioan Fericean
70d0e0171c better historyy agent starting 2014-01-23 18:22:50 +02:00
DanB
ba3cabb7bb Extra log in engine for history 2014-01-23 17:03:43 +01:00
DanB
5d857f1255 Fix local tests 2014-01-23 13:18:00 +01:00
DanB
4f3e91d8ca Default client-server over internal connections, small fixup order of service start 2014-01-23 12:45:13 +01:00
DanB
7c52e1e692 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-01-23 11:05:06 +01:00
Radu Ioan Fericean
81cc68e0c5 removed exit after cdrs 2014-01-23 11:59:05 +02:00
DanB
f51e0b10e3 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-01-23 10:51:03 +01:00
Radu Ioan Fericean
999ac0aead added server log messages 2014-01-23 11:44:14 +02:00
DanB
02297d4d36 Merge branch 'master' of https://github.com/cgrates/cgrates 2014-01-23 10:29:41 +01:00
DanB
95fde288ad Fixup mailer 2014-01-23 10:29:31 +01:00
Radu Ioan Fericean
ea75cd3aa2 cgr-engine main is async again 2014-01-23 11:25:32 +02:00
Radu Ioan Fericean
21875f7ed7 compiation fixes 2014-01-23 11:13:29 +02:00
Radu Ioan Fericean
7697b74713 Merge branch 'master' into shared_balances
Conflicts:
	apier/v1/apier.go
	cmd/cgr-engine/cgr-engine.go
	engine/storage_redis.go
2014-01-23 10:26:29 +02:00
DanB
46b87953da Fix test default config mailer 2014-01-22 23:28:05 +01:00
DanB
5e72e1c528 Adding mail_async action 2014-01-22 22:14:06 +01:00
DanB
baf590dab3 Renaming ActionTimings -> ActionPlans inside tutorial samples 2014-01-22 21:09:20 +01:00
DanB
2c3e8e1584 Adding mailer configuration 2014-01-22 21:06:57 +01:00
Radu Ioan Fericean
962cf17884 updated config file 2014-01-22 21:02:23 +02:00
Radu Ioan Fericean
ac60a39852 removed the async stuff from main and fixed method Lock has wrong number
of ins: 1 issue
2014-01-22 20:55:37 +02:00
Radu Ioan Fericean
0005ba2f68 RPC server changes
rpc server now serves both json and gob, refactored server code into
it's own type
2014-01-22 19:53:30 +02:00
DanB
08e5d8540a callUrlAsync using previous json encoded balance for error logging 2014-01-21 17:52:03 +01:00
DanB
6c96937059 Async call url logs failures 2014-01-21 13:31:53 +01:00
DanB
080395e7ea Adding async call_url action 2014-01-21 13:03:47 +01:00
DanB
2896053199 Renaming ActionsTimings into ActionPlan so we avoid in the future redis store changes as much as possible 2014-01-19 20:14:46 +01:00
DanB
0348be416c ActionTimings -> ActionPlan in APIs, storage tables and redis key 2014-01-19 20:02:32 +01:00
DanB
13707ca377 Increased cgrates version for next packaging 2014-01-18 19:47:00 +01:00
DanB
8ec403f151 Fixup reloading cache keys on csv/db load, removed automatic caching in db/set methods since it makes no sense/trouble in distributed environments 2014-01-18 19:45:23 +01:00
DanB
abd0a00ecb Fix packaging to uncompress freeswitch config 2014-01-18 09:12:59 +01:00
DanB
e97b72a30a Archiving FreeSWITCH tutorial configs since tools like ohloh.net are counting them as CGRateS sources 2014-01-18 08:53:52 +01:00
Radu Ioan Fericean
ea6337d9fa updated engine command 2014-01-14 19:23:05 +02:00
Radu Ioan Fericean
2a1f6ff583 Merge branch 'master' into shared_balances 2014-01-14 19:17:11 +02:00
Radu Ioan Fericean
b7cdf53858 Merge branch 'master' into shared_balances 2014-01-14 19:11:33 +02:00
Radu Ioan Fericean
99e269b894 Merge branch 'master' into shared_balances 2014-01-14 18:20:29 +02:00
Radu Ioan Fericean
c50a61fec5 vet fixing 2014-01-13 22:37:10 +02:00
Radu Ioan Fericean
d765e0090b Merge branch 'master' into shared_balances
Conflicts:
	engine/loader_csv.go
2014-01-13 22:34:19 +02:00
Radu Ioan Fericean
d776219013 added user info to balance for shared balances finding 2014-01-13 22:11:51 +02:00
Radu Ioan Fericean
7bc182e374 various go vet fixes 2014-01-13 17:30:06 +02:00
Radu Ioan Fericean
fb1c7265f5 Merge branch 'master' into shared_balances 2014-01-12 19:09:12 +02:00
Radu Ioan Fericean
ca6957cb48 Merge branch 'master' into shared_balances
Conflicts:
	engine/accountlock.go
2014-01-10 19:04:55 +02:00
Radu Ioan Fericean
1471c47bda started adding test for shared group and created utility methods for
getting shared balances
2014-01-09 20:06:32 +02:00
Radu Ioan Fericean
c8fb090c11 meged master branch 2014-01-09 17:21:27 +02:00
Radu Ioan Fericean
1c2d2116fd little improvement 2014-01-09 16:57:44 +02:00
Radu Ioan Fericean
3f16c1c12d debit minutes logic 2014-01-08 22:15:11 +02:00
Radu Ioan Fericean
9854ce7068 fixed build 2014-01-07 22:38:38 +02:00
Radu Ioan Fericean
58ddae9b70 Started shared balances logic 2014-01-07 22:34:40 +02:00
671 changed files with 10637 additions and 42547 deletions

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ docs/_*
bin
.idea
dean*
data/vagrant/.vagrant
data/vagrant/vagrant_ansible_inventory_default

View File

@@ -15,6 +15,6 @@ notifications:
on_success: change
on_failure: always
email:
on_success: change
on_success: never
on_failure: always

View File

@@ -1,5 +1,7 @@
## Rating system for Telecom & ISP environments ##
[![Build Status](https://drone.io/github.com/cgrates/cgrates/status.png)](https://drone.io/github.com/cgrates/cgrates/latest) [![Build Status](https://secure.travis-ci.org/cgrates/cgrates.png)](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.
[![Build Status](https://drone.io/github.com/cgrates/cgrates/status.png)](https://drone.io/github.com/cgrates/cgrates/latest) [![Build Status](https://secure.travis-ci.org/cgrates/cgrates.png)](http://travis-ci.org/cgrates/cgrates) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cgrates/cgrates/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
View 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
View 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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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))
}()
}

View 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))
}()
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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"))
}
*/
}

View File

@@ -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
View 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()
}

View File

@@ -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
View 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
View 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
View 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
}

View File

@@ -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")
}
}

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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())

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
}

View 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")
}
}

View File

@@ -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)

View File

@@ -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
View 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")
}
}

View File

@@ -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
View 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
View 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))
}
}

View File

@@ -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]
}

View File

@@ -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
}
}

View File

@@ -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
View 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
}

View File

@@ -20,7 +20,7 @@ package console
import (
"fmt"
"github.com/cgrates/cgrates/apier/v1"
"github.com/cgrates/cgrates/apier"
)
func init() {

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
}

View File

@@ -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

View 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>.

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
<configuration name="db.conf" description="LIMIT DB Configuration">
<settings>
<!--<param name="odbc-dsn" value="dsn:user:pass"/>-->
</settings>
</configuration>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
<configuration name="ivr.conf" description="IVR menus">
<menus>
<X-PRE-PROCESS cmd="include" data="../ivr_menus/*.xml"/>
</menus>
</configuration>

View File

@@ -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>

View File

@@ -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. -->

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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