Cyclops 4 HPC is the purpose built stack to support large HPC centers with resource accounting and billing of cluster as well as cloud resources.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

855 lines
21 KiB

package dbManager
import (
"encoding/json"
"errors"
"math"
"reflect"
"strings"
"time"
"github.com/go-openapi/strfmt"
"github.com/prometheus/client_golang/prometheus"
cdrModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/cdr/models"
"github.com/Cyclops-Labs/cyclops-4-hpc.git/services/credit-system/models"
"github.com/Cyclops-Labs/cyclops-4-hpc.git/services/credit-system/server/cacheManager"
cusModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/customerdb/models"
pmModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/plan-manager/models"
l "gitlab.com/cyclops-utilities/logging"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
const (
scaler = 1e7
statusDuplicated = iota
statusFail
statusMissing
statusOK
AC_CREDIT = "CREDIT"
AC_CASH = "CASH"
AC_BOTH = "BOTH"
AC_NONE = "NONE"
MED_CASH = "CASH"
MED_CREDIT = "CREDIT"
)
var (
//states = []string{"active", "error", "inactive", "suspended", "terminated"}
states = []string{"active", "error", "inactive", "used"}
totalTime float64
totalCount int64
)
// DbParameter is the struct defined to group and contain all the methods
// that interact with the database.
// On it there is the following parameters:
// - Cache: CacheManager pointer for the cache mechanism.
// - connStr: strings with the connection information to the database
// - Db: a gorm.DB pointer to the db to invoke all the db methods
type DbParameter struct {
Cache *cacheManager.CacheManager
connStr string
Db *gorm.DB
Metrics map[string]*prometheus.GaugeVec
}
// New is the function to create the struct DbParameter.
// Parameters:
// - dbConn: strings with the connection information to the database
// - tables: array of interfaces that will contains the models to migrate
// to the database on initialization
// Returns:
// - DbParameter: struct to interact with dbManager functionalities¬
func New(dbConn string, tables ...interface{}) *DbParameter {
l.Trace.Printf("[DB] Gerenating new DBParameter.\n")
var (
dp DbParameter
err error
)
dp.connStr = dbConn
dp.Db, err = gorm.Open(postgres.Open(dbConn), &gorm.Config{})
if err != nil {
l.Error.Printf("[DB] Error opening connection. Error: %v\n", err)
}
l.Trace.Printf("[DB] Migrating tables.\n")
//Database migration, it handles everything
dp.Db.AutoMigrate(tables...)
//l.Trace.Printf("[DB] Generating hypertables.\n")
// Hypertables creation for timescaledb in case of needed
//dp.Db.Exec("SELECT create_hypertable('" + dp.Db.NewScope(&models.TABLE).TableName() + "', 'TIMESCALE-ROW-INDEX');")
return &dp
}
// AddConsumption job is to decrease the credit in the system linked to the
// provided account by a certain amount of credit.
// This function is the one that produces a "cosumed" decrease of credit without
// human intervention.
// Parameters:
// - id: string containing the id of the account requested.
// - amount: float containing the amount to be -- in the system
// Returns:
// - reference to creditStatus containing the credit balance after the operation.
// - error raised in case of problems.
func (d *DbParameter) AddConsumption(id string, amount float64, medium string) (*models.CreditStatus, error) {
l.Trace.Printf("[DB] Attempting to add a comsuption of %v credit, in the account with id: %v", amount, id)
var c, c0, cs models.CreditStatus
var ce models.CreditEvents
var e error
if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&c).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
} else {
med := strings.ToUpper(medium)
if med == models.CreditEventsMediumCREDIT {
c0.AvailableCredit = c.AvailableCredit - amount
}
if med == models.CreditEventsMediumCASH {
c0.AvailableCash = c.AvailableCash - amount
}
c0.LastUpdate = strfmt.DateTime(time.Now())
ce.AccountID = id
ce.Delta = -amount
ce.EventType = func(s string) *string { return &s }(models.EventEventTypeConsumption)
ce.Timestamp = strfmt.DateTime(time.Now())
ce.Medium = &med
if e = d.Db.Model(&c).Updates(c0).Error; e == nil {
if e = d.Db.Create(&ce).Error; e == nil {
d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs)
} else {
l.Trace.Printf("[DB] Attempting to add the comsuption of credit event for account in the system failed.")
}
} else {
l.Trace.Printf("[DB] Attempting to update the account in the system failed.")
}
}
return &cs, e
}
// CreateAccount job is to register a new account in the system with the provided
// ID.
// Parameters:
// - id: string containing the id of the account to be created.
// Returns:
// - reference to AccountStatus containing the state of the account after the
// operation.
// - error raised in case of problems.
func (d *DbParameter) CreateAccount(id string) (*models.AccountStatus, error) {
l.Trace.Printf("[DB] Attempting to create a new account with id: %v", id)
var a, a0, as models.AccountStatus
var c models.CreditStatus
var e error
if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&a).Error; errors.Is(r, gorm.ErrRecordNotFound) {
a0.AccountID = id
a0.CreatedAt = strfmt.DateTime(time.Now())
c.AccountID = id
c.LastUpdate = strfmt.DateTime(time.Now())
if e = d.Db.Create(&a0).Error; e == nil {
if e = d.Db.Create(&c).Error; e == nil {
d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as)
d.Metrics["count"].With(prometheus.Labels{"type": "Accounts created"}).Inc()
} else {
l.Trace.Printf("[DB] Attempting to create the credit account in the system failed.")
}
} else {
l.Trace.Printf("[DB] Attempting to create the account in the system failed.")
}
} else {
l.Trace.Printf("[DB] Account with id: %v already in the system, check with administrator.", id)
e = errors.New("account already exist in the system")
}
return &as, e
}
// DecreaseCredit job is to decrease the credit in the system linked to the
// provided account by a certain amount of credit.
// Parameters:
// - id: string containing the id of the account requested.
// - amount: float containing the amount to be decreased in the system.
// Returns:
// - reference to CreditStatus containing the credit balance after the operation.
// - error raised in case of problems.
func (d *DbParameter) DecreaseCredit(id string, amount float64, medium string) (*models.CreditStatus, error) {
l.Trace.Printf("[DB] Attempting to decrease credit by: %v, in the account with id: %v", amount, id)
var c, c0, cs models.CreditStatus
var ce models.CreditEvents
var e error
if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&c).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
} else {
med := strings.ToUpper(medium)
if med == models.CreditEventsMediumCREDIT {
c0.AvailableCredit = c.AvailableCredit - amount
}
if med == models.CreditEventsMediumCASH {
c0.AvailableCash = c.AvailableCash - amount
}
c0.LastUpdate = strfmt.DateTime(time.Now())
ce.AccountID = id
ce.Delta = -amount
ce.EventType = func(s string) *string { return &s }(models.EventEventTypeAuthorizedDecrease)
ce.Timestamp = strfmt.DateTime(time.Now())
ce.Medium = &med
if e = d.Db.Model(&c).Updates(c0).Error; e == nil {
if e = d.Db.Create(&ce).Error; e == nil {
d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs)
} else {
l.Trace.Printf("[DB] Attempting to add the decrease of credit event for account in the system failed.")
}
} else {
l.Trace.Printf("[DB] Attempting to update the account in the system failed.")
}
}
return &cs, e
}
// DisableAccount job is to mark as disabled in the system the account provided.
// Parameters:
// - id: string containing the id of the account to be disabled.
// Returns:
// - reference to AccountStatus containing the state of the account after the
// operation.
// - error raised in case of problems.
func (d *DbParameter) DisableAccount(id string) (*models.AccountStatus, error) {
l.Trace.Printf("[DB] Attempting to disable account with id: %v", id)
var a, as models.AccountStatus
var e error
if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&a).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
} else {
enabled := false
if e = d.Db.Model(&a).Updates(&models.AccountStatus{Enabled: enabled}).Error; e == nil {
d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as)
} else {
l.Trace.Printf("[DB] Attempting to update the account in the system failed.")
}
}
return &as, e
}
// EnableAccount job is to mark as enabled in the system the account provided.
// Parameters:
// - id: string containing the id of the account to be enabled.
// Returns:
// - reference to AccountStatus containing the state of the account after the
// operation.
// - error raised in case of problems.
func (d *DbParameter) EnableAccount(id string) (*models.AccountStatus, error) {
l.Trace.Printf("[DB] Attempting to enable account with id: %v", id)
var a, as models.AccountStatus
var e error
if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&a).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
} else {
if e = d.Db.Model(&a).Update("Enabled", true).Error; e == nil {
d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as)
} else {
l.Trace.Printf("[DB] Attempting to update the account in the system failed.")
}
}
return &as, e
}
// GetAccountStatus job is to retrieve the actual state of the provided account.
// Parameters:
// - id: string containing the id of the account to be retrieved.
// Returns:
// - reference to AccountStatus containing the state of the account.
// - error raised in case of problems.
func (d *DbParameter) GetAccountStatus(id string) (*models.AccountStatus, error) {
l.Trace.Printf("[DB] Attempting to get the status of the account with id: %v", id)
var as models.AccountStatus
var e error
if r := d.Db.Where(&models.AccountStatus{AccountID: id}).First(&as).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
}
return &as, e
}
// GetCredit job is to retrieve the actual state of the credit balance of the
// provided account.
// Parameters:
// - id: string containing the id of the account requested.
// Returns:
// - reference to CreditStatus containing the credit balance.
// - error raised in case of problems.
func (d *DbParameter) GetCredit(id string) (*models.CreditStatus, error) {
l.Trace.Printf("[DB] Attempting to get the credit balance in the account with id: %v", id)
var cs models.CreditStatus
var e error
if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
}
return &cs, e
}
// GetHistory job is to retrieve the history of the credit balance of the account
// provided with the posibility of filter the actions.
// Parameters:
// - id: string containing the id of the account requested.
// - filtered: string specifying the criteria for filtering.
// Returns:
// - reference to CreditHistory containing the credit balance history contained
// in the system.
// - error raised in case of problems.
func (d *DbParameter) GetHistory(id string, filtered bool, medium string) (*models.CreditHistory, error) {
l.Trace.Printf("[DB] Attempting to get the credit history in the account with id: %v", id)
var ch models.CreditHistory
var ce []*models.CreditEvents
var e0 []*models.Event
var e error
ch.AccountID = id
med := strings.ToUpper(medium)
if filtered {
evType := models.EventEventTypeConsumption
e = d.Db.Where(&models.CreditEvents{AccountID: id, Medium: &med}).Not(&models.CreditEvents{EventType: &evType}).Find(&ce).Error
} else {
e = d.Db.Where(&models.CreditEvents{AccountID: id, Medium: &med}).Find(&ce).Error
}
if e != nil {
l.Trace.Printf("[DB] Account with id: %v doesn't have events in the system, check with administrator.", id)
}
for _, event := range ce {
var ev models.Event
ev.Delta = event.Delta
ev.EventType = event.EventType
ev.Timestamp = event.Timestamp
ev.AuthorizedBy = event.AuthorizedBy
e0 = append(e0, &ev)
}
l.Trace.Printf("[DB] Amount of events for the account with id: %v, is: %v.", id, len(e0))
ch.Events = e0
return &ch, e
}
// IncreaseCredit job is to increase the credit in the system linked to the
// provided account by a certain amount of credit.
// Parameters:
// - id: string containing the id of the account requested.
// - amount: float containing the amount to be -- in the system
// Returns:
// - reference to CreditStatus containing the credit balance after the operation.
// - error raised in case of problems.
func (d *DbParameter) IncreaseCredit(id string, amount float64, medium string) (*models.CreditStatus, error) {
l.Trace.Printf("[DB] Attempting to increase credit by: %v, in the account with id: %v", amount, id)
var c, c0, cs models.CreditStatus
var ce models.CreditEvents
var e error
if r := d.Db.Where(&models.CreditStatus{AccountID: id}).First(&c).Error; errors.Is(r, gorm.ErrRecordNotFound) {
l.Trace.Printf("[DB] Account with id: %v doesn't exist in the system, check with administrator.", id)
e = errors.New("account doesn't exist in the system")
} else {
med := strings.ToUpper(medium)
if med == models.CreditEventsMediumCREDIT {
c0.AvailableCredit = c.AvailableCredit + amount
}
if med == models.CreditEventsMediumCASH {
c0.AvailableCash = c.AvailableCash + amount
}
c0.LastUpdate = strfmt.DateTime(time.Now())
ce.AccountID = id
ce.Delta = amount
ce.EventType = func(s string) *string { return &s }(models.EventEventTypeAuthorizedIncrease)
ce.Timestamp = strfmt.DateTime(time.Now())
ce.Medium = &med
if e = d.Db.Model(&c).Updates(c0).Error; e == nil {
if e = d.Db.Create(&ce).Error; e == nil {
d.Db.Where(&models.CreditStatus{AccountID: id}).First(&cs)
} else {
l.Trace.Printf("[DB] Attempting to add the increase of credit event for account in the system failed.")
}
} else {
l.Trace.Printf("[DB] Attempting to update the account in the system failed.")
}
}
return &cs, e
}
// ListAccounts job is to retrieve the list of all the accounts safe in the system.
// Returns:
// - slice of references to AccountStatus containing the status of every account
// in the system
func (d *DbParameter) ListAccounts() ([]*models.AccountStatus, error) {
l.Trace.Printf("[DB] Attempting to list accounts in the system.")
var as []*models.AccountStatus
var e error
if e = d.Db.Find(&as).Error; e != nil {
l.Trace.Printf("[DB] There is not accounts in the system")
}
return as, e
}
// ProcessCDR job is to check every CDR report arriving to the system to add the
// consumptions of each account to the corresponding
// Parameters:
// - report: CDR Report model reference to be processed.
// - usage: a boolean discriminant to set the accounting to usage instead of cost
// Returns:
// - e: error in case of problem while doing a conversion, a cache.get, or adding the comsumption.
func (d *DbParameter) ProcessCDR(report cdrModels.CReport, token string) error {
l.Trace.Printf("[DB] Starting the processing of the a CDR.\n")
var planID string
now := time.Now().UnixNano()
listAccounts, e := d.ListAccounts()
if e != nil {
l.Warning.Printf("[DB] There's was an error retrieving the accounts in the system. Error: %v\n", e)
return e
}
if len(listAccounts) < 1 {
l.Trace.Printf("[DB] There's no accounts in the system right now, skipping the processing...\n")
return nil
}
p, e := d.Cache.Get(report.AccountID, "product", token)
if e != nil {
l.Warning.Printf("[DB] Something went wrong while retrieving the product info for id [ %v ]. Error: %v\n", report.AccountID, e)
return e
}
product := p.(cusModels.Product)
c, e := d.Cache.Get(product.CustomerID, "customer", token)
if e != nil {
l.Warning.Printf("[DB] Something went wrong while retrieving the customer info for id [ %v ]. Error: %v\n", product.CustomerID, e)
return e
}
customer := c.(cusModels.Customer)
account, e := d.GetAccountStatus(customer.CustomerID)
if e != nil {
l.Trace.Printf("[DB] There's no account associated in the customer level ID: [ %v ], moving to the next level...\n", customer.CustomerID)
acc, e := d.GetAccountStatus(customer.ResellerID)
if e != nil {
l.Trace.Printf("[DB] There's no account associated in the reseller level. ID: [ %v ], skipping this product [ %v ].\n", customer.ResellerID, product.ProductID)
return nil
}
account = acc
}
if !(account.Enabled) {
l.Trace.Printf("[DB] Account associated [ %v ] is disabled! Skipping...\n", account.AccountID)
return nil
}
if product.PlanID != "" {
planID = product.PlanID
} else {
if customer.PlanID != "" {
planID = customer.PlanID
} else {
r, e := d.Cache.Get(customer.ResellerID, "reseller", token)
if e != nil {
l.Warning.Printf("[DB] Something went wrong while retrieving the reseller info for id [ %v ]. Error: %v\n", customer.ResellerID, e)
return e
}
reseller := r.(cusModels.Reseller)
planID = reseller.PlanID
}
}
PlanDefault:
p, e = d.Cache.Get(planID, "plan", token)
if e != nil {
if planID == "DEFAULT" {
l.Warning.Printf("[DB] Something went wrong while retrieving the default plan id [ %v ]. Error: %v\n", planID, e)
return e
}
l.Warning.Printf("[DB] Something went wrong while retrieving the plan id [ %v ]. Re-trying with default plan. Error: %v\n", planID, e)
planID = "DEFAULT"
goto PlanDefault
}
plan := p.(pmModels.Plan)
// In case the plan is not valid we return to the deault plan (id 0) which is valid ad vitam
if (time.Now()).After((time.Time)(*plan.OfferedEndDate)) || (time.Now()).Before((time.Time)(*plan.OfferedStartDate)) {
l.Warning.Printf("[DB] The plan [ %v ] is only valid between [ %v ] and [ %v ]. Falling back to default plan.\n", plan.ID, *plan.OfferedStartDate, *plan.OfferedEndDate)
planID = "DEFAULT"
goto PlanDefault
}
skus := make(map[string]*pmModels.SkuPrice)
for _, k := range plan.SkuPrices {
skus[k.SkuName] = k
}
var cashDelta, creditDelta float64
for i := range report.Usage {
var value interface{}
mode := *skus[report.Usage[i].ResourceType].AccountingMode
if mode == AC_CREDIT || mode == AC_BOTH {
for _, j := range states {
value = report.Usage[i].UsageBreakup[j]
if value == nil {
l.Debug.Printf("[DB] The state [ %v ] seem to not be part of the usage UsageBreakup [ %+v ], skipping...", j, report.Usage[i].UsageBreakup)
continue
}
if k := reflect.ValueOf(value); k.Kind() == reflect.Float64 {
creditDelta += value.(float64) * skus[report.Usage[i].ResourceType].UnitCreditPrice
continue
}
v, e := value.(json.Number).Float64()
if e != nil {
l.Warning.Printf("[DB] There was a problem retrieving the float value from the usagebreakup interface. Error: %v.\n", e)
return e
}
creditDelta += v * skus[report.Usage[i].ResourceType].UnitCreditPrice
}
}
if mode == AC_CASH || mode == AC_BOTH {
value = report.Usage[i].Cost["totalFromSku"]
if k := reflect.ValueOf(value); k.Kind() == reflect.Float64 {
cashDelta += value.(float64)
continue
}
v, e := value.(json.Number).Float64()
if e != nil {
l.Warning.Printf("[DB] There was a problem retrieving the float value from the cost interface. Error: %v.\n", e)
return e
}
cashDelta += v
}
}
state, e := d.AddConsumption(account.AccountID, d.getNiceFloat(creditDelta), MED_CREDIT)
if e != nil {
l.Warning.Printf("[DB] There was a problem while adding the consumption to the account [ %v ]. Error: %v.\n", account.AccountID, e)
return e
}
if state.AvailableCredit < float64(0) {
l.Warning.Printf("[DB] Account [ %v ] credit is not positive. Credit: [ %v ].\n", account.AccountID, state.AvailableCredit)
}
l.Trace.Printf("[DB] Account [ %v ] has been updated with a [ %v ] consumption.\n", account.AccountID, creditDelta)
state, e = d.AddConsumption(account.AccountID, d.getNiceFloat(cashDelta), MED_CASH)
if e != nil {
l.Warning.Printf("[DB] There was a problem while adding the consumption to the account [ %v ]. Error: %v.\n", account.AccountID, e)
return e
}
if state.AvailableCash < float64(0) {
l.Warning.Printf("[DB] Account [ %v ] cash is not positive. Cash: [ %v ].\n", account.AccountID, state.AvailableCash)
}
l.Trace.Printf("[DB] Account [ %v ] has been updated with a [ %v ] consumption.\n", account.AccountID, cashDelta)
totalTime += float64(time.Now().UnixNano()-now) / float64(time.Millisecond)
totalCount++
d.Metrics["count"].With(prometheus.Labels{"type": "CDRs procesed"}).Inc()
d.Metrics["time"].With(prometheus.Labels{"type": "CDRs average processing time"}).Set(totalTime / float64(totalCount))
return nil
}
func (d *DbParameter) getNiceFloat(i float64) (o float64) {
return float64(math.Round(i*scaler) / scaler)
}