From eda80242eb41c0c2918e224c413b9a61cc49bb49 Mon Sep 17 00:00:00 2001 From: arberkatellari Date: Tue, 4 Nov 2025 16:39:56 +0200 Subject: [PATCH] Make Charger Profiles storable in MySQL and Postgres --- chargers/chargers_it_test.go | 4 +- config/config_defaults.go | 4 +- config/configsanity.go | 3 +- .../samples/apis_chargers_mysql/cgrates.json | 3 +- .../apis_chargers_postgres/cgrates.json | 54 +++++++++++++++ .../samples/apis_chargers_redis/cgrates.json | 53 +++++++++++++++ data/conf/samples/tutmysql/cgrates.json | 2 +- data/conf/samples/tutpostgres/cgrates.json | 2 +- data/storage/mysql/create_db_tables.sql | 13 +++- data/storage/postgres/create_db_tables.sql | 11 ++++ engine/models.go | 11 ++++ engine/storage_sql.go | 65 ++++++++++++++----- utils/chargers.go | 35 ++++++++++ utils/consts.go | 45 ++++++------- 14 files changed, 257 insertions(+), 48 deletions(-) create mode 100644 data/conf/samples/apis_chargers_postgres/cgrates.json create mode 100644 data/conf/samples/apis_chargers_redis/cgrates.json diff --git a/chargers/chargers_it_test.go b/chargers/chargers_it_test.go index 1267c3af3..a214ded8e 100644 --- a/chargers/chargers_it_test.go +++ b/chargers/chargers_it_test.go @@ -86,11 +86,11 @@ func TestChargersIT(t *testing.T) { case utils.MetaMongo: chargersConfigDIR = "apis_chargers_mongo" case utils.MetaRedis: - t.SkipNow() + chargersConfigDIR = "apis_chargers_redis" case utils.MetaMySQL: chargersConfigDIR = "apis_chargers_mysql" case utils.MetaPostgres: - t.SkipNow() + chargersConfigDIR = "apis_chargers_postgres" default: t.Fatal("Unknown Database type") } diff --git a/config/config_defaults.go b/config/config_defaults.go index 98b68a311..1b020482e 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -130,6 +130,8 @@ const CGRATES_CFG_JSON = ` "*ip_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*ip_allocations": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*action_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, + "*versions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, + "*charger_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, // compatible db types: <*internal|*redis|*mongo> "*actions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, @@ -142,10 +144,8 @@ const CGRATES_CFG_JSON = ` "*filters": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*route_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*attribute_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, - "*charger_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*rate_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*load_ids": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, - "*versions": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "*default"}, "*resource_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"}, "*ip_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"}, "*stat_filter_indexes" : {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate": false, "dbConn": "*default"}, diff --git a/config/configsanity.go b/config/configsanity.go index f2ac78afe..0b8a8da46 100644 --- a/config/configsanity.go +++ b/config/configsanity.go @@ -1061,7 +1061,8 @@ func (cfg *CGRConfig) checkConfigSanity() error { utils.Internal, utils.Redis, utils.Mongo} if item != utils.MetaAccounts && item != utils.MetaIPProfiles && - item != utils.MetaIPAllocations && item != utils.MetaActionProfiles { + item != utils.MetaIPAllocations && item != utils.MetaActionProfiles && + item != utils.MetaChargerProfiles { if item == utils.MetaCDRs { if !slices.Contains(storDBTypes, cfg.dbCfg.DBConns[val.DBConn].Type) { return fmt.Errorf("<%s> db item can only be of types <%v>, got <%s>", item, diff --git a/data/conf/samples/apis_chargers_mysql/cgrates.json b/data/conf/samples/apis_chargers_mysql/cgrates.json index 23e13d9ab..764de414e 100644 --- a/data/conf/samples/apis_chargers_mysql/cgrates.json +++ b/data/conf/samples/apis_chargers_mysql/cgrates.json @@ -33,7 +33,8 @@ } }, "items": { - "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*charger_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} } }, diff --git a/data/conf/samples/apis_chargers_postgres/cgrates.json b/data/conf/samples/apis_chargers_postgres/cgrates.json new file mode 100644 index 000000000..aef5c443b --- /dev/null +++ b/data/conf/samples/apis_chargers_postgres/cgrates.json @@ -0,0 +1,54 @@ +{ + +"general": { + "reply_timeout": "50s" +}, + +"logger": { + "level": 7 +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { // The id of the DB connection + "db_type": "postgres", // db type: + "db_host": "127.0.0.1", + "db_port": 5432, // db port to reach the database + "db_name": "cgrates", // the host to connect to + "db_user": "cgrates", + "db_password": "CGRateS.org" // password to use when connecting to the database + }, + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*charger_profiles": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } +}, + +"attributes": { + "enabled": true +}, + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"] +}, + +"admins": { + "enabled": true +} + +} \ No newline at end of file diff --git a/data/conf/samples/apis_chargers_redis/cgrates.json b/data/conf/samples/apis_chargers_redis/cgrates.json new file mode 100644 index 000000000..23e13d9ab --- /dev/null +++ b/data/conf/samples/apis_chargers_redis/cgrates.json @@ -0,0 +1,53 @@ +{ + +"general": { + "reply_timeout": "50s" +}, + +"logger": { + "level": 7 +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"db": { + "db_conns": { + "*default": { + "db_type": "redis", + "db_host": "127.0.0.1", + "db_port": 6379, + "db_name": "10", + "db_user": "cgrates" + }, + "StorDB": { + "db_type": "mysql", + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "cgrates", + "db_user": "cgrates", + "db_password": "CGRateS.org" + } + }, + "items": { + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} + } +}, + +"attributes": { + "enabled": true +}, + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"] +}, + +"admins": { + "enabled": true +} + +} \ No newline at end of file diff --git a/data/conf/samples/tutmysql/cgrates.json b/data/conf/samples/tutmysql/cgrates.json index 7e832905f..31b7a0631 100644 --- a/data/conf/samples/tutmysql/cgrates.json +++ b/data/conf/samples/tutmysql/cgrates.json @@ -36,7 +36,7 @@ }, }, "items": { - "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} } }, diff --git a/data/conf/samples/tutpostgres/cgrates.json b/data/conf/samples/tutpostgres/cgrates.json index fe2cdd0fe..c8e6d80d9 100644 --- a/data/conf/samples/tutpostgres/cgrates.json +++ b/data/conf/samples/tutpostgres/cgrates.json @@ -36,7 +36,7 @@ }, "items": { "*cdrs": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, - "*accounts": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"}, + "*accounts": {"limit": -1, "ttl": "", "static_ttl": false, "remote":false, "replicate":false, "dbConn": "StorDB"} } }, diff --git a/data/storage/mysql/create_db_tables.sql b/data/storage/mysql/create_db_tables.sql index 7ad05d027..55d5396f0 100644 --- a/data/storage/mysql/create_db_tables.sql +++ b/data/storage/mysql/create_db_tables.sql @@ -44,4 +44,15 @@ CREATE TABLE action_profiles ( PRIMARY KEY (`pk`), UNIQUE KEY unique_tenant_id (`tenant`, `id`) ); -CREATE UNIQUE INDEX action_profiles_idx ON action_profiles (`id`); \ No newline at end of file +CREATE UNIQUE INDEX action_profiles_idx ON action_profiles (`id`); + +DROP TABLE IF EXISTS charger_profiles; +CREATE TABLE charger_profiles ( + `pk` int(11) NOT NULL AUTO_INCREMENT, + `tenant` VARCHAR(40) NOT NULL, + `id` VARCHAR(64) NOT NULL, + `charger_profile` JSON NOT NULL, + PRIMARY KEY (`pk`), + UNIQUE KEY unique_tenant_id (`tenant`, `id`) +); +CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles (`id`); \ No newline at end of file diff --git a/data/storage/postgres/create_db_tables.sql b/data/storage/postgres/create_db_tables.sql index a4e9183d8..b12166b3d 100644 --- a/data/storage/postgres/create_db_tables.sql +++ b/data/storage/postgres/create_db_tables.sql @@ -43,3 +43,14 @@ CREATE TABLE action_profiles ( UNIQUE (tenant, id) ); CREATE UNIQUE INDEX action_profiles_idx ON action_profiles ("id"); + + +DROP TABLE IF EXISTS charger_profiles; +CREATE TABLE charger_profiles ( + pk SERIAL PRIMARY KEY, + tenant VARCHAR(40) NOT NULL, + id VARCHAR(64) NOT NULL, + charger_profile JSONB NOT NULL, + UNIQUE (tenant, id) +); +CREATE UNIQUE INDEX charger_profiles_idx ON charger_profiles ("id"); diff --git a/engine/models.go b/engine/models.go index 341fff727..8152485f4 100644 --- a/engine/models.go +++ b/engine/models.go @@ -426,3 +426,14 @@ type ActionProfileJSONMdl struct { func (ActionProfileJSONMdl) TableName() string { return utils.TBLActionProfilesJSON } + +type ChargerProfileMdl struct { + PK uint `gorm:"primary_key"` + Tenant string `index:"0" re:".*"` + ID string `index:"1" re:".*"` + ChargerProfile utils.JSONB `gorm:"type:jsonb" index:"2" re:".*"` +} + +func (ChargerProfileMdl) TableName() string { + return utils.TBLChargerProfilesJSON +} diff --git a/engine/storage_sql.go b/engine/storage_sql.go index 1814be9ce..37f646246 100644 --- a/engine/storage_sql.go +++ b/engine/storage_sql.go @@ -106,6 +106,8 @@ func (sqls *SQLStorage) GetKeysForPrefix(ctx *context.Context, prefix string) (k keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLIPAllocations, tntID) case utils.ActionProfilePrefix: keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLActionProfilesJSON, tntID) + case utils.ChargerProfilePrefix: + keys, err = sqls.getAllKeysMatchingTenantID(ctx, utils.TBLChargerProfilesJSON, tntID) default: err = fmt.Errorf("unsupported prefix in GetKeysForPrefix: %q", prefix) } @@ -534,8 +536,7 @@ func (sqls *SQLStorage) GetActionProfileDrv(ctx *context.Context, tenant, id str if len(result) == 0 { return nil, utils.ErrNotFound } - ap, err = utils.MapStringInterfaceToActionProfile(result[0].ActionProfile) - return + return utils.MapStringInterfaceToActionProfile(result[0].ActionProfile) } func (sqls *SQLStorage) SetActionProfileDrv(ctx *context.Context, ap *utils.ActionProfile) (err error) { @@ -570,6 +571,51 @@ func (sqls *SQLStorage) RemoveActionProfileDrv(ctx *context.Context, tenant, id return nil } +func (sqls *SQLStorage) GetChargerProfileDrv(_ *context.Context, tenant, id string) (cp *utils.ChargerProfile, err error) { + var result []*ChargerProfileMdl + if err := sqls.db.Model(&ChargerProfileMdl{}).Where(&ChargerProfileMdl{Tenant: tenant, + ID: id}).Find(&result).Error; err != nil { + return nil, err + } + if len(result) == 0 { + return nil, utils.ErrNotFound + } + + return utils.MapStringInterfaceToChargerProfile(result[0].ChargerProfile) +} + +func (sqls *SQLStorage) SetChargerProfileDrv(_ *context.Context, cp *utils.ChargerProfile) (err error) { + tx := sqls.db.Begin() + mdl := &ChargerProfileMdl{ + Tenant: cp.Tenant, + ID: cp.ID, + ChargerProfile: cp.AsMapStringInterface(), + } + if err := tx.Model(&ChargerProfileMdl{}).Where( + ChargerProfileMdl{Tenant: mdl.Tenant, ID: mdl.ID}).Delete( + ChargerProfileMdl{}).Error; err != nil { + tx.Rollback() + return err + } + if err := tx.Save(mdl).Error; err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func (sqls *SQLStorage) RemoveChargerProfileDrv(_ *context.Context, tenant, id string) (err error) { + tx := sqls.db.Begin() + if err := tx.Model(&ChargerProfileMdl{}).Where(&ChargerProfileMdl{Tenant: tenant, ID: id}). + Delete(&ChargerProfileMdl{}).Error; err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + // AddLoadHistory DataDB method not implemented yet func (sqls *SQLStorage) AddLoadHistory(ldInst *utils.LoadInstance, loadHistSize int, transactionID string) error { @@ -807,21 +853,6 @@ func (sqls *SQLStorage) RemoveAttributeProfileDrv(ctx *context.Context, tenant, return utils.ErrNotImplemented } -// DataDB method not implemented yet -func (sqls *SQLStorage) GetChargerProfileDrv(_ *context.Context, tenant, id string) (r *utils.ChargerProfile, err error) { - return nil, utils.ErrNotImplemented -} - -// DataDB method not implemented yet -func (sqls *SQLStorage) SetChargerProfileDrv(_ *context.Context, r *utils.ChargerProfile) (err error) { - return utils.ErrNotImplemented -} - -// DataDB method not implemented yet -func (sqls *SQLStorage) RemoveChargerProfileDrv(_ *context.Context, tenant, id string) (err error) { - return utils.ErrNotImplemented -} - // GetStorageType returns the storage type that is being used func (sqls *SQLStorage) GetStorageType() string { return utils.MetaMySQL diff --git a/utils/chargers.go b/utils/chargers.go index 10182c571..9c304792d 100644 --- a/utils/chargers.go +++ b/utils/chargers.go @@ -173,3 +173,38 @@ type ChargerProfileWithAPIOpts struct { *ChargerProfile APIOpts map[string]any } + +// AsMapStringInterface converts ChargerProfile struct to map[string]any +func (cp *ChargerProfile) AsMapStringInterface() map[string]any { + if cp == nil { + return nil + } + return map[string]any{ + Tenant: cp.Tenant, + ID: cp.ID, + FilterIDs: cp.FilterIDs, + Weights: cp.Weights, + Blockers: cp.Blockers, + RunID: cp.RunID, + AttributeIDs: cp.AttributeIDs, + } +} + +// MapStringInterfaceToChargerProfile converts map[string]any to ChargerProfile struct +func MapStringInterfaceToChargerProfile(m map[string]any) (cp *ChargerProfile, err error) { + cp = &ChargerProfile{} + if v, ok := m[Tenant].(string); ok { + cp.Tenant = v + } + if v, ok := m[ID].(string); ok { + cp.ID = v + } + cp.FilterIDs = InterfaceToStringSlice(m[FilterIDs]) + cp.Weights = InterfaceToDynamicWeights(m[Weights]) + cp.Blockers = InterfaceToDynamicBlockers(m[Blockers]) + if v, ok := m[RunID].(string); ok { + cp.RunID = v + } + cp.AttributeIDs = InterfaceToStringSlice(m[AttributeIDs]) + return cp, nil +} diff --git a/utils/consts.go b/utils/consts.go index 8446f81b9..ff1b5dcab 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1897,28 +1897,29 @@ const ( // Table Name const ( - TBLTPResources = "tp_resources" - TBLTPStats = "tp_stats" - TBLTPRankings = "tp_rankings" - TBLTPTrends = "tp_trends" - TBLTPThresholds = "tp_thresholds" - TBLTPFilters = "tp_filters" - SessionCostsTBL = "session_costs" - CDRsTBL = "cdrs" - TBLTPRoutes = "tp_routes" - TBLTPAttributes = "tp_attributes" - TBLTPChargers = "tp_chargers" - TBLVersions = "versions" - TBLAccounts = "accounts" - TBLIPProfiles = "ip_profiles" - TBLIPAllocations = "ip_allocations" - TBLActionProfilesJSON = "action_profiles" - OldSMCosts = "sm_costs" - TBLTPDispatchers = "tp_dispatcher_profiles" - TBLTPDispatcherHosts = "tp_dispatcher_hosts" - TBLTPRateProfiles = "tp_rate_profiles" - TBLTPActionProfiles = "tp_action_profiles" - TBLTPAccounts = "tp_accounts" + TBLTPResources = "tp_resources" + TBLTPStats = "tp_stats" + TBLTPRankings = "tp_rankings" + TBLTPTrends = "tp_trends" + TBLTPThresholds = "tp_thresholds" + TBLTPFilters = "tp_filters" + SessionCostsTBL = "session_costs" + CDRsTBL = "cdrs" + TBLTPRoutes = "tp_routes" + TBLTPAttributes = "tp_attributes" + TBLTPChargers = "tp_chargers" + TBLVersions = "versions" + TBLAccounts = "accounts" + TBLIPProfiles = "ip_profiles" + TBLIPAllocations = "ip_allocations" + TBLActionProfilesJSON = "action_profiles" + TBLChargerProfilesJSON = "charger_profiles" + OldSMCosts = "sm_costs" + TBLTPDispatchers = "tp_dispatcher_profiles" + TBLTPDispatcherHosts = "tp_dispatcher_hosts" + TBLTPRateProfiles = "tp_rate_profiles" + TBLTPActionProfiles = "tp_action_profiles" + TBLTPAccounts = "tp_accounts" ) // Cache Name