/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package migrator import ( "errors" "fmt" "log" "strings" "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" ) const ( v1AccountDBPrefix = "ubl_" v1AccountTBL = "userbalances" ) func (m *Migrator) migrateCurrentAccounts() (err error) { var ids []string ids, err = m.dmIN.DataManager().DataDB().GetKeysForPrefix(utils.AccountPrefix, utils.EmptyString) if err != nil { return err } for _, id := range ids { idg := strings.TrimPrefix(id, utils.AccountPrefix) acc, err := m.dmIN.DataManager().GetAccount(idg) if err != nil { return err } if acc == nil || m.dryRun { continue } if err := m.dmOut.DataManager().SetAccount(acc); err != nil { return err } if err := m.dmIN.DataManager().RemoveAccount(idg); err != nil { return err } m.stats[utils.Accounts]++ } return } func (m *Migrator) removeV1Accounts() (err error) { var v1Acnt *v1Account for { v1Acnt, err = m.dmIN.getv1Account() if err != nil && err != utils.ErrNoMoreData { return err } if err == utils.ErrNoMoreData { break } if err = m.dmIN.remV1Account(v1Acnt.Id); err != nil { return err } } return } func (m *Migrator) migrateV1Accounts() (v3Acnt *engine.Account, err error) { var v1Acnt *v1Account v1Acnt, err = m.dmIN.getv1Account() if err != nil { return nil, err } else if v1Acnt == nil { return nil, errors.New("Account is nil") } v3Acnt = v1Acnt.V1toV3Account() return } func (m *Migrator) removeV2Accounts() (err error) { var v2Acnt *v2Account for { v2Acnt, err = m.dmIN.getv2Account() if err != nil && err != utils.ErrNoMoreData { return err } if err == utils.ErrNoMoreData { break } if err = m.dmIN.remV2Account(v2Acnt.ID); err != nil { return } } return } func (m *Migrator) migrateV2Accounts() (v3Acnt *engine.Account, err error) { var v2Acnt *v2Account v2Acnt, err = m.dmIN.getv2Account() if err != nil { return nil, err } else if v2Acnt == nil { return nil, errors.New("Account is nil") } v3Acnt = v2Acnt.V2toV3Account() return } func (m *Migrator) migrateAccounts() (err error) { var vrs engine.Versions current := engine.CurrentDataDBVersions() if vrs, err = m.getVersions(utils.Accounts); err != nil { return } migrated := true migratedFrom := 0 var v3Acnt *engine.Account for { version := vrs[utils.Accounts] migratedFrom = int(version) for { switch version { default: return fmt.Errorf("Unsupported version %v", version) case current[utils.Accounts]: migrated = false if m.sameDataDB { break } if err = m.migrateCurrentAccounts(); err != nil { return } version = 3 case 1: //migrate v1 to v3 if v3Acnt, err = m.migrateV1Accounts(); err != nil && err != utils.ErrNoMoreData { return err } else if err == utils.ErrNoMoreData { break } version = 3 case 2: //migrate v2 to v3 if v3Acnt, err = m.migrateV2Accounts(); err != nil && err != utils.ErrNoMoreData { return } else if err == utils.ErrNoMoreData { break } version = 3 } if version == current[utils.Accounts] || err == utils.ErrNoMoreData { break } } if err == utils.ErrNoMoreData || !migrated { break } if !m.dryRun { if err = m.dmOut.DataManager().SetAccount(v3Acnt); err != nil { return } } m.stats[utils.Accounts]++ } if m.dryRun || !migrated { return nil } // Remove old accounts from dbIn (only if dbIn != dbOut ) if !m.sameDataDB { switch migratedFrom { case 1: if err = m.removeV1Accounts(); err != nil && err != utils.ErrNoMoreData { return } case 2: if err = m.removeV2Accounts(); err != nil && err != utils.ErrNoMoreData { return } } } // All done, update version wtih current one if err = m.setVersions(utils.Accounts); err != nil { return err } return m.ensureIndexesDataDB(engine.ColAcc) } type v1Account struct { Id string BalanceMap map[string]v1BalanceChain UnitCounters []*v1UnitsCounter ActionTriggers v1ActionTriggers AllowNegative bool Disabled bool } type v1BalanceChain []*v1Balance type v1Balance struct { Uuid string //system wide unique Id string // account wide unique Value float64 ExpirationDate time.Time Weight float64 DestinationIds string RatingSubject string Category string SharedGroup string Timings []*engine.RITiming TimingIDs string Disabled bool } type v1UnitsCounter struct { Direction string BalanceType string Balances v1BalanceChain // first balance is the general one (no destination) } type v2Account struct { ID string BalanceMap map[string]engine.Balances UnitCounters engine.UnitCounters ActionTriggers engine.ActionTriggers AllowNegative bool Disabled bool } func (b *v1Balance) IsDefault() bool { return (b.DestinationIds == "" || b.DestinationIds == utils.MetaAny) && b.RatingSubject == "" && b.Category == "" && b.ExpirationDate.IsZero() && b.SharedGroup == "" && b.Weight == 0 && !b.Disabled } func (v1Acc v1Account) V1toV3Account() (ac *engine.Account) { // transfer data into new structure ac = &engine.Account{ ID: v1Acc.Id, BalanceMap: make(map[string]engine.Balances, len(v1Acc.BalanceMap)), UnitCounters: make(engine.UnitCounters, len(v1Acc.UnitCounters)), ActionTriggers: make(engine.ActionTriggers, len(v1Acc.ActionTriggers)), AllowNegative: v1Acc.AllowNegative, Disabled: v1Acc.Disabled, } idElements := strings.Split(ac.ID, utils.ConcatenatedKeySep) if len(idElements) != 3 { log.Printf("Malformed account ID %s", v1Acc.Id) } ac.ID = utils.ConcatenatedKey(idElements[1], idElements[2]) // balances for oldBalKey, oldBalChain := range v1Acc.BalanceMap { keyElements := strings.Split(oldBalKey, utils.Meta) newBalKey := utils.Meta + keyElements[1] ac.BalanceMap[newBalKey] = make(engine.Balances, len(oldBalChain)) for index, oldBal := range oldBalChain { balVal := oldBal.Value if newBalKey == utils.MetaVoice { balVal = utils.Round(balVal/float64(time.Second), config.CgrConfig().GeneralCfg().RoundingDecimals, utils.MetaRoundingMiddle) } // check default to set new id ac.BalanceMap[newBalKey][index] = &engine.Balance{ Uuid: oldBal.Uuid, ID: oldBal.Id, Value: balVal, ExpirationDate: oldBal.ExpirationDate, Weight: oldBal.Weight, DestinationIDs: utils.ParseStringMap(oldBal.DestinationIds), RatingSubject: oldBal.RatingSubject, Categories: utils.ParseStringMap(oldBal.Category), SharedGroups: utils.ParseStringMap(oldBal.SharedGroup), Timings: oldBal.Timings, TimingIDs: utils.ParseStringMap(oldBal.TimingIDs), Disabled: oldBal.Disabled, Factors: engine.ValueFactors{}, } } } // unit counters for _, oldUc := range v1Acc.UnitCounters { newUc := &engine.UnitCounter{Counters: make(engine.CounterFilters, len(oldUc.Balances))} for index, oldUcBal := range oldUc.Balances { bf := &engine.BalanceFilter{} if oldUcBal.Uuid != "" { bf.Uuid = utils.StringPointer(oldUcBal.Uuid) } if oldUcBal.Id != "" { bf.ID = utils.StringPointer(oldUcBal.Id) } if oldUc.BalanceType != "" { bf.Type = utils.StringPointer(oldUc.BalanceType) } if !oldUcBal.ExpirationDate.IsZero() { bf.ExpirationDate = utils.TimePointer(oldUcBal.ExpirationDate) } if oldUcBal.Weight != 0 { bf.Weight = utils.Float64Pointer(oldUcBal.Weight) } if oldUcBal.DestinationIds != "" { bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.DestinationIds)) } if oldUcBal.RatingSubject != "" { bf.RatingSubject = utils.StringPointer(oldUcBal.RatingSubject) } if oldUcBal.Category != "" { bf.Categories = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.Category)) } if oldUcBal.SharedGroup != "" { bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.SharedGroup)) } if oldUcBal.TimingIDs != "" { bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(oldUcBal.TimingIDs)) } if oldUcBal.Disabled { bf.Disabled = utils.BoolPointer(oldUcBal.Disabled) } bf.Timings = oldUcBal.Timings cf := &engine.CounterFilter{ Value: oldUcBal.Value, Filter: bf, } newUc.Counters[index] = cf } ac.UnitCounters[oldUc.BalanceType] = append(ac.UnitCounters[oldUc.BalanceType], newUc) } //action triggers for index, oldAtr := range v1Acc.ActionTriggers { at := &engine.ActionTrigger{ UniqueID: oldAtr.Id, ThresholdType: oldAtr.ThresholdType, ThresholdValue: oldAtr.ThresholdValue, Recurrent: oldAtr.Recurrent, MinSleep: oldAtr.MinSleep, Weight: oldAtr.Weight, ActionsID: oldAtr.ActionsId, MinQueuedItems: oldAtr.MinQueuedItems, Executed: oldAtr.Executed, } bf := &engine.BalanceFilter{} if oldAtr.BalanceId != "" { bf.ID = utils.StringPointer(oldAtr.BalanceId) } if oldAtr.BalanceType != "" { bf.Type = utils.StringPointer(oldAtr.BalanceType) } if oldAtr.BalanceRatingSubject != "" { bf.RatingSubject = utils.StringPointer(oldAtr.BalanceRatingSubject) } if oldAtr.BalanceDestinationIds != "" { bf.DestinationIDs = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceDestinationIds)) } if oldAtr.BalanceTimingTags != "" { bf.TimingIDs = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceTimingTags)) } if oldAtr.BalanceCategory != "" { bf.Categories = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceCategory)) } if oldAtr.BalanceSharedGroup != "" { bf.SharedGroups = utils.StringMapPointer(utils.ParseStringMap(oldAtr.BalanceSharedGroup)) } if oldAtr.BalanceWeight != 0 { bf.Weight = utils.Float64Pointer(oldAtr.BalanceWeight) } if !oldAtr.BalanceExpirationDate.IsZero() { bf.ExpirationDate = utils.TimePointer(oldAtr.BalanceExpirationDate) } at.Balance = bf ac.ActionTriggers[index] = at if ac.ActionTriggers[index].ThresholdType == "*min_counter" || ac.ActionTriggers[index].ThresholdType == "*max_counter" { ac.ActionTriggers[index].ThresholdType = strings.Replace(ac.ActionTriggers[index].ThresholdType, "_", "_event_", 1) } } return } func (v2Acc v2Account) V2toV3Account() (ac *engine.Account) { ac = &engine.Account{ ID: v2Acc.ID, BalanceMap: make(map[string]engine.Balances, len(v2Acc.BalanceMap)), UnitCounters: make(engine.UnitCounters, len(v2Acc.UnitCounters)), ActionTriggers: make(engine.ActionTriggers, len(v2Acc.ActionTriggers)), AllowNegative: v2Acc.AllowNegative, Disabled: v2Acc.Disabled, } // balances for balType, oldBalChain := range v2Acc.BalanceMap { ac.BalanceMap[balType] = make(engine.Balances, len(oldBalChain)) for index, oldBal := range oldBalChain { balVal := oldBal.Value if balType == utils.MetaVoice { balVal = utils.Round(balVal*float64(time.Second), config.CgrConfig().GeneralCfg().RoundingDecimals, utils.MetaRoundingMiddle) } // check default to set new id ac.BalanceMap[balType][index] = &engine.Balance{ Uuid: oldBal.Uuid, ID: oldBal.ID, Value: balVal, ExpirationDate: oldBal.ExpirationDate, Weight: oldBal.Weight, DestinationIDs: oldBal.DestinationIDs, RatingSubject: oldBal.RatingSubject, Categories: oldBal.Categories, SharedGroups: oldBal.SharedGroups, Timings: oldBal.Timings, TimingIDs: oldBal.TimingIDs, Disabled: oldBal.Disabled, Factors: oldBal.Factors, } } } // unit counters ac.UnitCounters = v2Acc.UnitCounters //action triggers ac.ActionTriggers = v2Acc.ActionTriggers return }