diff --git a/cmd/cgr-loader/actions.go b/cmd/cgr-loader/actions.go index e4ae4ff5b..24e5542b2 100644 --- a/cmd/cgr-loader/actions.go +++ b/cmd/cgr-loader/actions.go @@ -19,10 +19,8 @@ along with this program. If not, see package main import ( - "encoding/csv" "github.com/cgrates/cgrates/timespans" "log" - "os" "fmt" "strconv" ) @@ -34,16 +32,14 @@ var ( accountActions []*timespans.UserBalance ) -func loadActions() { - fp, err := os.Open(*actionsFn) +func (csvr *CSVReader) loadActions(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open actions file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tag" { @@ -104,16 +100,14 @@ func loadActions() { log.Print(actions) } -func loadActionTimings() { - fp, err := os.Open(*actiontimingsFn) +func (csvr *CSVReader) loadActionTimings(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open actions timings file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tag" { @@ -151,16 +145,14 @@ func loadActionTimings() { log.Print(actionsTimings) } -func loadActionTriggers() { - fp, err := os.Open(*actiontriggersFn) +func (csvr *CSVReader) loadActionTriggers(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open destination balance actions file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tag" { @@ -190,16 +182,14 @@ func loadActionTriggers() { log.Print(actionsTriggers) } -func loadAccountActions() { - fp, err := os.Open(*accountactionsFn) +func (csvr *CSVReader) loadAccountActions(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open account actions file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { if record[0] == "Tenant" { continue diff --git a/cmd/cgr-loader/cgr-loader.go b/cmd/cgr-loader/cgr-loader.go index c9ce8a010..d9d43330b 100644 --- a/cmd/cgr-loader/cgr-loader.go +++ b/cmd/cgr-loader/cgr-loader.go @@ -22,12 +22,15 @@ import ( "flag" "github.com/cgrates/cgrates/timespans" "log" + "encoding/csv" + "strings" + "os" ) var ( separator = flag.String("separator", ",", "Default field separator") - redisserver = flag.String("redisserver", "127.0.0.1:6379", "redis server address (tcp:127.0.0.1:6379)") - redisdb = flag.Int("rdb", 10, "redis database number (10)") + redissrv = flag.String("redissrv", "127.0.0.1:6379", "redis server address (tcp:127.0.0.1:6379)") + redisdb = flag.Int("redisdb", 10, "redis database number (10)") redispass = flag.String("pass", "", "redis database password") flush = flag.Bool("flush", false, "Flush the database before importing") monthsFn = flag.String("month", "Months.csv", "Months file") @@ -46,7 +49,7 @@ var ( ) func writeToDatabase() { - storage, err := timespans.NewRedisStorage(*redisserver, *redisdb) + storage, err := timespans.NewRedisStorage(*redissrv, *redisdb) if err != nil { log.Fatalf("Could not open database connection: %v", err) } @@ -82,17 +85,40 @@ func writeToDatabase() { } } +func openFileCSVReader(fn string) (csvReader *csv.Reader, fp *os.File, err error) { + fp, err = os.Open(fn) + if err != nil { + return + } + csvReader = csv.NewReader(fp) + csvReader.Comma = sep + csvReader.TrailingComma = true + return +} + +func openStringCSVReader(data string) (csvReader *csv.Reader, fp *os.File, err error) { + csvReader = csv.NewReader(strings.NewReader(data)) + csvReader.Comma = ',' + csvReader.TrailingComma = true + return +} + +type CSVReader struct { + readerFunc func(string) (*csv.Reader, *os.File, error) +} + func main() { flag.Parse() sep = []rune(*separator)[0] - loadDestinations() - loadRates() - loadTimings() - loadRateTimings() - loadRatingProfiles() - loadActions() - loadActionTimings() - loadActionTriggers() - loadAccountActions() + csvr := &CSVReader{openFileCSVReader} + csvr.loadDestinations(*destinationsFn) + csvr.loadRates(*ratesFn) + csvr.loadTimings(*timingsFn) + csvr.loadRateTimings(*ratetimingsFn) + csvr.loadRatingProfiles(*ratingprofilesFn) + csvr.loadActions(*actionsFn) + csvr.loadActionTimings(*actiontimingsFn) + csvr.loadActionTriggers(*actiontriggersFn) + csvr.loadAccountActions(*accountactionsFn) writeToDatabase() } diff --git a/cmd/cgr-loader/loader_test.go b/cmd/cgr-loader/loader_test.go new file mode 100644 index 000000000..476bc34ee --- /dev/null +++ b/cmd/cgr-loader/loader_test.go @@ -0,0 +1,140 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012 Radu Ioan Fericean + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package main + +import ( + "testing" +) + +var ( + dest = ` +Tag,Prefix +NAT,0256 +NAT,0257 +NAT,0723 +RET,0723 +RET,0724 +` + rts = ` +P1,NAT,0,1,1 +P2,NAT,0,0.5,1 +` + ts = ` +WORKDAYS_00,*all,*all,1;2;3;4;5,00:00:00 +WORKDAYS_18,*all,*all,1;2;3;4;5,18:00:00 +WEEKENDS,*all,*all,6;7,00:00:00 +ONE_TIME_RUN,*none,*none,*none,*now +` + rtts = ` +EVENING,P1,WORKDAYS_00,10 +EVENING,P2,WORKDAYS_18,10 +EVENING,P2,WEEKENDS,10 +` + rp = ` +vdf,0,OUT,rif,,EVENING,2012-01-01T00:00:00Z +vdf,0,OUT,rif,,EVENING,2012-02-28T00:00:00Z +vdf,0,OUT,minu,,EVENING,2012-01-01T00:00:00Z +vdf,0,OUT,minu,,EVENING,2012-02-28T00:00:00Z +` + a = ` +MINI,TOPUP,MINUTES,100,NAT,ABSOLUTE,0,10,10 +` + atms = ` +MORE_MINUTES,MINI,ONE_TIME_RUN,10 +` + atrs = ` +STANDARD_TRIGGER,MINUTES,10,GERMANY_O2,SOME_1,10 +STANDARD_TRIGGER,MINUTES,200,GERMANY,SOME_2,10 +` + accs = ` +vdf,minitsboy,OUT,MORE_MINUTES,STANDARD_TRIGGER +` +) + +func TestDestinations(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadDestinations(dest) + if len(destinations) != 2 { + t.Error("Failed to load destinations: ", destinations) + } +} + +func TestRates(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadRates(rts) + if len(rates) != 2 { + t.Error("Failed to load rates: ", rates) + } +} + +func TestTimimgs(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadTimings(ts) + if len(timings) != 4 { + t.Error("Failed to load timings: ", timings) + } +} + +func TestRateTimings(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadRateTimings(rtts) + if len(activationPeriods) != 1 { + t.Error("Failed to load rate timings: ", activationPeriods) + } +} + +func TestRatingProfiles(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadRatingProfiles(rp) + if len(ratingProfiles) != 4 { + t.Error("Failed to load rating profiles: ", ratingProfiles) + } +} + +func TestActions(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadActions(a) + if len(actions) != 1 { + t.Error("Failed to load actions: ", actions) + } +} + +func TestActionTimings(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadActionTimings(atms) + if len(actionsTimings) != 1 { + t.Error("Failed to load action timings: ", actionsTimings) + } +} + +func TestActionTriggers(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadActionTriggers(atrs) + if len(actionsTriggers) != 1 { + t.Error("Failed to load action triggers: ", actionsTriggers) + } +} + +func TestAccountActions(t *testing.T) { + csvr := &CSVReader{openStringCSVReader} + csvr.loadAccountActions(accs) + if len(accountActions) != 1 { + t.Error("Failed to load account actions: ", accountActions) + } +} diff --git a/cmd/cgr-loader/rates.go b/cmd/cgr-loader/rates.go index a24fe4aae..e80395fdf 100644 --- a/cmd/cgr-loader/rates.go +++ b/cmd/cgr-loader/rates.go @@ -19,11 +19,9 @@ along with this program. If not, see package main import ( - "encoding/csv" "github.com/cgrates/cgrates/timespans" "log" "fmt" - "os" "time" ) @@ -36,17 +34,16 @@ var ( ratingProfiles = make(map[string]CallDescriptors) ) -func loadDestinations() { - fp, err := os.Open(*destinationsFn) +func (csvr *CSVReader) loadDestinations(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open destinations file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { + tag := record[0] if tag == "Tag" { // skip header line @@ -63,20 +60,18 @@ func loadDestinations() { dest = ×pans.Destination{Id: tag} destinations = append(destinations, dest) } - dest.Prefixes = append(dest.Prefixes, record[1:]...) + dest.Prefixes = append(dest.Prefixes, record[1]) } } -func loadRates() { - fp, err := os.Open(*ratesFn) +func (csvr *CSVReader) loadRates(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open rates timing file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tag" { @@ -91,16 +86,14 @@ func loadRates() { } } -func loadTimings() { - fp, err := os.Open(*timingsFn) +func (csvr *CSVReader) loadTimings(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open timings file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tag" { @@ -113,16 +106,14 @@ func loadTimings() { } } -func loadRateTimings() { - fp, err := os.Open(*ratetimingsFn) +func (csvr *CSVReader) loadRateTimings(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open rates timings file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tag" { @@ -153,16 +144,14 @@ func loadRateTimings() { } } -func loadRatingProfiles() { - fp, err := os.Open(*ratingprofilesFn) +func (csvr *CSVReader) loadRatingProfiles(fn string) { + csvReader, fp, err := csvr.readerFunc(fn) if err != nil { - log.Printf("Could not open destinations rates file: %v", err) return } - defer fp.Close() - csvReader := csv.NewReader(fp) - csvReader.Comma = sep - csvReader.TrailingComma = true + if fp != nil { + defer fp.Close() + } for record, err := csvReader.Read(); err == nil; record, err = csvReader.Read() { tag := record[0] if tag == "Tenant" { @@ -232,7 +221,7 @@ func loadRatingProfiles() { log.Printf("Could not open destinations rates file: %v", err) return } - defer fp.Close() + if fp != nil {defer fp.Close()} csvReader := csv.NewReader(fp) csvReader.Comma = sep csvReader.TrailingComma = true diff --git a/docs/apicalls.rst b/docs/apicalls.rst index 537ebe74d..091630420 100644 --- a/docs/apicalls.rst +++ b/docs/apicalls.rst @@ -1,5 +1,47 @@ Api Calls ======== +The general API usage of the CGRateS involves creating a CallDescriptor structure sending it to the balancer via JSON/GOB RPC and getting a response from the balancer in form of a CallCost structure or a numeric value for requested information. + +CallDescriptor structure +------------------------ + - TOR int + - CstmId, Subject, DestinationPrefix string + - TimeStart, TimeEnd time.Time + - Amount float64 +TOR + Type Of Record, used to differentiate between various type of records +CstmId + Customer Identification used for multi tenant databases +Subject + Subject for this query +DestinationPrefix + Destination prefix to be matched +TimeStart, TimeEnd + The start end end of the call in question +Amount + The amount requested in various API calls (e.g. DebitSMS amount) + +CallCost structure +------------------ + - TOR int + - CstmId, Subject, DestinationPrefix string + - Cost, ConnectFee float64 + - Timespans []*TimeSpan +TOR + Type Of Record, used to differentiate between various type of records (for query identification and confirmation) +CstmId + Customer Identification used for multi tenant databases (for query identification and confirmation) +Subject + Subject for this query (for query identification and confirmation) +DestinationPrefix + Destination prefix to be matched (for query identification and confirmation) +Cost + The requested cost +ConnectFee + The requested connection cost +Timespans + The timespans in witch the initial TimeStart-TimeEnd was split in for cost determination with all pricing and cost information attached. + As stated before the balancer (or the rater directly) can be accesed via json rpc. The smallest python snippet to acces the CGRateS balancer is this: diff --git a/docs/importing.rst b/docs/importing.rst index cf487914a..d82cd84fc 100644 --- a/docs/importing.rst +++ b/docs/importing.rst @@ -1,3 +1,4 @@ +.. _`data-importing`: Data importing ============= diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b36497bac..35b24a787 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -1,58 +1,18 @@ Tutorial ======== -The general usage of the CGRateS involves creating a CallDescriptor structure sending it to the balancer via JSON RPC and getting a response from the balancer inf form of a CallCost structure or a numeric value for requested information. The general steps to get up and running with CGRateS are: -#. Create JSON files containing rates, budgets, tariff plans and destinations, see :ref:`data-importing`. +#. Create CSV files containing the initial data for CGRateS, see :ref:`data-importing`. #. Load the data in the databases using the loader tool. -#. Start the balancer, see :ref:`running`. +#. Start the balancer or rater and connect it to the call switch, see :ref:`running`. #. Start one ore more raters. -#. Make API calls to the balancer/rater. - -CallDescriptor structure ------------------------- - - TOR int - - CstmId, Subject, DestinationPrefix string - - TimeStart, TimeEnd time.Time - - Amount float64 -TOR - Type Of Record, used to differentiate between various type of records -CstmId - Customer Identification used for multi tenant databases -Subject - Subject for this query -DestinationPrefix - Destination prefix to be matched -TimeStart, TimeEnd - The start end end of the call in question -Amount - The amount requested in various API calls (e.g. DebitSMS amount) - -CallCost structure ------------------- - - TOR int - - CstmId, Subject, DestinationPrefix string - - Cost, ConnectFee float64 - - Timespans []*TimeSpan -TOR - Type Of Record, used to differentiate between various type of records (for query identification and confirmation) -CstmId - Customer Identification used for multi tenant databases (for query identification and confirmation) -Subject - Subject for this query (for query identification and confirmation) -DestinationPrefix - Destination prefix to be matched (for query identification and confirmation) -Cost - The requested cost -ConnectFee - The requested connection cost -Timespans - The timespans in witch the initial TimeStart-TimeEnd was split in for cost determination with all pricing and cost information attached. +#. Make API calls to the balancer/rater or just let the session manager do the work. Instalation ----------- **Using packages** + **Using source** After the go environment is installed_ and setup_ just issue the following commands: @@ -62,8 +22,6 @@ After the go environment is installed_ and setup_ just issue the following comma This will install the sources and compile all available tools -After that navigate - .. _installed: http://golang.org/doc/install .. _setup: http://golang.org/doc/code.html @@ -79,9 +37,13 @@ cgr-balancer rif@grace:~$ cgr-balancer --help Usage of cgr-balancer: - -httpapiaddr="127.0.0.1:8000": HTTP API server address (localhost:2002) - -jsonrpcaddr="127.0.0.1:2001": JSON RPC server address (localhost:2001) - -rateraddr="127.0.0.1:2000": Rater server address (localhost:2000) + -freeswitchpass="ClueCon": freeswitch address host:port + -freeswitchsrv="localhost:8021": freeswitch address host:port + -httpapiaddr="127.0.0.1:8000": Http API server address (localhost:2002) + -json=false: use JSON for RPC encoding + -jsonrpcaddr="127.0.0.1:2001": Json RPC server address (localhost:2001) + -rateraddr="127.0.0.1:2000": Rater server address (localhost:2000) + cgr-rater The cgr-rater can be provided with the balancer server address and can be configured to listen to a specific interface and port. @@ -90,8 +52,14 @@ cgr-rater rif@grace:~$ cgr-rater --help Usage of cgr-rater: -balancer="127.0.0.1:2000": balancer address host:port - -json=false: use json for rpc encoding + -freeswitch=false: connect to freeswitch server + -freeswitchpass="ClueCon": freeswitch address host:port + -freeswitchsrv="localhost:8021": freeswitch address host:port + -json=false: use JSON for RPC encoding -listen="127.0.0.1:1234": listening address host:port + -redisdb=10: redis database number + -redissrv="127.0.0.1:6379": redis address host:port + -standalone=false: start standalone server (no balancer) cgr-console The cgr-console is a command line tool used to access the balancer (or the rater directly) to call all the API methods offered by CGRateS. @@ -108,20 +76,6 @@ cgr-console -tor=0: Type of record -ts="2012-02-09T00:00:00Z": Time start - rif@grace:~$ cgr-cgrates - List of commands: - getcost - getmaxsessiontime - debitbalance - debitsms - debitseconds - addvolumediscountseconds - resetvolumediscountseconds - addrecievedcallseconds - resetuserbudget - status - - cgr-loader The loader is the most configurable tool because it has options for each of the three supported databases (kyoto, redis and mongodb). Apart from that multi-database options it is quite easy to be used. @@ -133,17 +87,50 @@ cgr-loader rif@grace:~$ cgr-loader --help Usage of cgr-loader: - -apfile="ap.json": Activation Periods containing intervals file - -destfile="dest.json": Destinations file - -kyotofile="storage.kch": kyoto storage file (storage.kch) - -mdb="test": mongo database name (test) - -mongoserver="127.0.0.1:27017": mongo server address (127.0.0.1:27017) + -accountactions="AccountActions.csv": Account actions file + -actions="Actions.csv": Actions file + -actiontimings="ActionTimings.csv": Actions timings file + -actiontriggers="ActionTriggers.csv": Actions triggers file + -destinations="Destinations.csv": Destinations file + -flush=false: Flush the database before importing + -month="Months.csv": Months file + -monthdays="MonthDays.csv": Month days file -pass="": redis database password + -rates="Rates.csv": Rates file + -ratetimings="RateTimings.csv": Rates timings file + -ratingprofiles="RatingProfiles.csv": Rating profiles file -rdb=10: redis database number (10) - -redisserver="tcp:127.0.0.1:6379": redis server address (tcp:127.0.0.1:6379) - -storage="all": kyoto|redis|mongo - -tpfile="tp.json": Tariff plans file - -ubfile="ub.json": User budgets file + -redisserver="127.0.0.1:6379": redis server address (tcp:127.0.0.1:6379) + -separator=",": Default field separator + -timings="Timings.csv": Timings file + -weekdays="WeekDays.csv": Week days file +rif@grace:~$ cgr-balancer --help +Usage of cgr-balancer: + -freeswitchpass="ClueCon": freeswitch address host:port + -freeswitchsrv="localhost:8021": freeswitch address host:port + -httpapiaddr="127.0.0.1:8000": Http API server address (localhost:2002) + -json=false: use JSON for RPC encoding + -jsonrpcaddr="127.0.0.1:2001": Json RPC server address (localhost:2001) + -rateraddr="127.0.0.1:2000": Rater server address (localhost:2000) +rif@grace:~$ cgr-sessionmanager --help +Usage of cgr-sessionmanager: + -balancer="127.0.0.1:2000": balancer address host:port + -freeswitchpass="ClueCon": freeswitch address host:port + -freeswitchsrv="localhost:8021": freeswitch address host:port + -json=false: use JSON for RPC encoding + -redisdb=10: redis database number + -redissrv="127.0.0.1:6379": redis address host:port + -standalone=false: run standalone (run as a rater) + +rif@grace:~$ cgr-mediator --help +Usage of cgr-mediator: + -dbname="cgrates": The name of the database to connect to. + -freeswitchcdr="Master.csv": Freeswitch Master CSV CDR file. + -host="localhost": The host to connect to. Values that start with / are for unix domain sockets. + -password="": The user's password. + -port="5432": The port to bind to. + -resultfile="out.csv": Generated file containing CDR and price info. + -user="": The user to sign in as. \ No newline at end of file diff --git a/timespans/destinations.go b/timespans/destinations.go index 0e1c0b01f..31d840fd2 100644 --- a/timespans/destinations.go +++ b/timespans/destinations.go @@ -64,6 +64,15 @@ func (d *Destination) containsPrefix(prefix string) (bool, int) { return false, 0 } +func (d *Destination) String() (result string) { + result = d.Id + ": " + for _, p := range d.Prefixes { + result += p + ", " + } + result = strings.TrimRight(result, ", ") + return result +} + func (d *Destination) store() (result string) { for _, p := range d.Prefixes { result += p + ","