billingresourcecheck.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. package models
  2. import (
  3. "context"
  4. "fmt"
  5. "reflect"
  6. "time"
  7. "yunion.io/x/jsonutils"
  8. "yunion.io/x/log"
  9. "yunion.io/x/pkg/errors"
  10. "yunion.io/x/pkg/util/billing"
  11. "yunion.io/x/sqlchemy"
  12. billing_api "yunion.io/x/onecloud/pkg/apis/billing"
  13. api "yunion.io/x/onecloud/pkg/apis/compute"
  14. notifyapi "yunion.io/x/onecloud/pkg/apis/notify"
  15. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  16. "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient"
  17. "yunion.io/x/onecloud/pkg/compute/options"
  18. "yunion.io/x/onecloud/pkg/mcclient"
  19. "yunion.io/x/onecloud/pkg/mcclient/auth"
  20. "yunion.io/x/onecloud/pkg/mcclient/modules/notify"
  21. "yunion.io/x/onecloud/pkg/util/logclient"
  22. )
  23. type SBillingResourceCheckManager struct {
  24. db.SVirtualResourceBaseManager
  25. }
  26. var BillingResourceCheckManager *SBillingResourceCheckManager
  27. func init() {
  28. BillingResourceCheckManager = &SBillingResourceCheckManager{
  29. SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
  30. SBillingResourceCheck{},
  31. "billing_resource_checks_tbl",
  32. "billing_resource_check",
  33. "billing_resource_checks",
  34. ),
  35. }
  36. BillingResourceCheckManager.SetVirtualObject(BillingResourceCheckManager)
  37. }
  38. type SBillingResourceCheck struct {
  39. db.SVirtualResourceBase
  40. SBillingResourceBase
  41. ResourceType string `width:"36" charset:"ascii" list:"user"`
  42. }
  43. type IBillingModelManager interface {
  44. db.IModelManager
  45. GetExpiredModels(advanceDay int) ([]IBillingModel, error)
  46. }
  47. type IBillingModel interface {
  48. db.IModel
  49. GetOwnerId() mcclient.IIdentityProvider
  50. GetStatus() string
  51. GetExpiredAt() time.Time
  52. GetReleaseAt() time.Time
  53. GetAutoRenew() bool
  54. GetCreatedAt() time.Time
  55. SetReleaseAt(releaseAt time.Time)
  56. SetExpiredAt(expireAt time.Time)
  57. SetBillingCycle(billingCycle string)
  58. SetBillingType(billingType billing_api.TBillingType)
  59. GetBillingType() billing_api.TBillingType
  60. }
  61. // 即将到期释放资源列表
  62. func (manager *SBillingResourceCheckManager) ListItemFilter(
  63. ctx context.Context,
  64. q *sqlchemy.SQuery,
  65. userCred mcclient.TokenCredential,
  66. query api.BillingResourceCheckListInput,
  67. ) (*sqlchemy.SQuery, error) {
  68. var err error
  69. q, err = manager.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput)
  70. if err != nil {
  71. return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter")
  72. }
  73. if len(query.ResourceType) > 0 {
  74. q = q.In("resource_type", query.ResourceType)
  75. }
  76. return q, nil
  77. }
  78. func SaveReleaseAt(ctx context.Context, model IBillingModel, userCred mcclient.TokenCredential, releaseAt time.Time) error {
  79. diff, err := db.Update(model, func() error {
  80. model.SetReleaseAt(releaseAt)
  81. return nil
  82. })
  83. if err != nil {
  84. return errors.Wrap(err, "Update")
  85. }
  86. if len(diff) > 0 {
  87. db.OpsLog.LogEvent(model, db.ACT_SET_RELEASE_TIME, fmt.Sprintf("release at: %s", releaseAt), userCred)
  88. }
  89. if len(diff) > 0 && userCred != nil {
  90. logclient.AddActionLogWithContext(ctx, model, logclient.ACT_SET_RELEASE_TIME, diff, userCred, true)
  91. }
  92. return nil
  93. }
  94. func SaveRenewInfo(
  95. ctx context.Context, userCred mcclient.TokenCredential,
  96. model IBillingModel, bc *billing.SBillingCycle, expireAt *time.Time, billingType billing_api.TBillingType,
  97. ) error {
  98. _, err := db.Update(model, func() error {
  99. if len(billingType) == 0 {
  100. billingType = billing_api.BILLING_TYPE_PREPAID
  101. }
  102. if model.GetBillingType() == "" {
  103. model.SetBillingType(billingType)
  104. }
  105. if expireAt != nil && !expireAt.IsZero() {
  106. model.SetExpiredAt(*expireAt)
  107. } else if bc != nil {
  108. model.SetBillingCycle(bc.String())
  109. model.SetExpiredAt(bc.EndAt(model.GetExpiredAt()))
  110. }
  111. return nil
  112. })
  113. if err != nil {
  114. log.Errorf("UpdateItem error %s", err)
  115. return err
  116. }
  117. db.OpsLog.LogEvent(model, db.ACT_RENEW, model.GetShortDesc(ctx), userCred)
  118. return nil
  119. }
  120. func (manager *SBillingResourceCheckManager) GetExpiredModels(advanceDay int) ([]SBillingResourceCheck, error) {
  121. upLimit := time.Now().AddDate(0, 0, advanceDay+1)
  122. downLimit := time.Now().AddDate(0, 0, advanceDay)
  123. q := manager.Query()
  124. q = q.Filter(
  125. sqlchemy.OR(
  126. sqlchemy.AND(
  127. sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_POSTPAID),
  128. sqlchemy.LE(q.Field("release_at"), upLimit),
  129. sqlchemy.GE(q.Field("release_at"), downLimit),
  130. ),
  131. // 跳过自动续费实例
  132. sqlchemy.AND(
  133. sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_PREPAID),
  134. sqlchemy.LE(q.Field("expired_at"), upLimit),
  135. sqlchemy.GE(q.Field("expired_at"), downLimit),
  136. sqlchemy.IsFalse(q.Field("auto_renew")),
  137. )))
  138. ret := []SBillingResourceCheck{}
  139. err := db.FetchModelObjects(manager, q, &ret)
  140. if err != nil {
  141. return nil, errors.Wrap(err, "FetchModelObjects")
  142. }
  143. return ret, nil
  144. }
  145. func fetchExpiredModels(manager db.IModelManager, advanceDay int) ([]IBillingModel, error) {
  146. upLimit := time.Now().AddDate(0, 0, advanceDay+1)
  147. downLimit := time.Now()
  148. v := reflect.MakeSlice(reflect.SliceOf(manager.TableSpec().DataType()), 0, 0)
  149. q := manager.Query()
  150. q = q.Filter(
  151. sqlchemy.OR(
  152. sqlchemy.AND(
  153. sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_POSTPAID),
  154. sqlchemy.LE(q.Field("release_at"), upLimit),
  155. sqlchemy.GE(q.Field("release_at"), downLimit),
  156. ),
  157. sqlchemy.AND(
  158. sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_PREPAID),
  159. sqlchemy.LE(q.Field("expired_at"), upLimit),
  160. sqlchemy.GE(q.Field("expired_at"), downLimit),
  161. )))
  162. vp := reflect.New(v.Type())
  163. vp.Elem().Set(v)
  164. err := db.FetchModelObjects(manager, q, vp.Interface())
  165. if err != nil {
  166. return nil, errors.Wrapf(err, "unable to list %s", manager.KeywordPlural())
  167. }
  168. v = vp.Elem()
  169. if v.Len() > 0 {
  170. log.Debugf("%s length of v: %d will be notified", manager.Alias(), v.Len())
  171. }
  172. ms := make([]IBillingModel, v.Len())
  173. for i := range ms {
  174. ms[i] = v.Index(i).Addr().Interface().(IBillingModel)
  175. }
  176. return ms, nil
  177. }
  178. func (bm *SBillingResourceCheckManager) Create(ctx context.Context, res IBillingModel, resourceType string) error {
  179. bc := &SBillingResourceCheck{
  180. ResourceType: resourceType,
  181. }
  182. bc.Id = res.GetId()
  183. bc.Name = res.GetName()
  184. bc.ExpiredAt = res.GetExpiredAt()
  185. bc.ReleaseAt = res.GetReleaseAt()
  186. bc.AutoRenew = res.GetAutoRenew()
  187. bc.BillingType = res.GetBillingType()
  188. if owner := res.GetOwnerId(); owner != nil {
  189. bc.ProjectId = owner.GetProjectId()
  190. bc.DomainId = owner.GetDomainId()
  191. }
  192. bc.CreatedAt = res.GetCreatedAt()
  193. bc.Status = res.GetStatus()
  194. bc.SetModelManager(bm, bc)
  195. return bm.TableSpec().InsertOrUpdate(ctx, bc)
  196. }
  197. func (man *SBillingResourceCheckManager) PerformCheck(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) {
  198. scanExpiredBillingResources(ctx)
  199. return jsonutils.NewDict(), nil
  200. }
  201. func (man *SBillingResourceCheckManager) clean(resType string, resourceIds []string) error {
  202. ids, err := db.FetchField(man, "id", func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
  203. return q.Equals("resource_type", resType).NotIn("id", resourceIds)
  204. })
  205. if err != nil {
  206. return errors.Wrap(err, "fetchField")
  207. }
  208. if len(ids) == 0 {
  209. return nil
  210. }
  211. return db.Purge(man, "id", ids, true)
  212. }
  213. func scanExpiredBillingResources(ctx context.Context) {
  214. billingResourceManagers := []IBillingModelManager{
  215. GuestManager,
  216. DBInstanceManager,
  217. ElasticcacheManager,
  218. }
  219. for _, manager := range billingResourceManagers {
  220. expiredModels, err := manager.GetExpiredModels(30)
  221. if err != nil {
  222. log.Errorf("unable to fetchExpiredModels: %v", err)
  223. continue
  224. }
  225. resourceIds := []string{}
  226. for _, model := range expiredModels {
  227. err = BillingResourceCheckManager.Create(ctx, model, manager.Keyword())
  228. if err != nil {
  229. log.Errorf("unable to create billing_resource_check for resource %s %s", manager.Keyword(), model.GetId())
  230. continue
  231. }
  232. resourceIds = append(resourceIds, model.GetId())
  233. }
  234. err = BillingResourceCheckManager.clean(manager.Keyword(), resourceIds)
  235. if err != nil {
  236. log.Errorf("unable to clean billing_resource_check for resource %s", manager.Keyword())
  237. continue
  238. }
  239. }
  240. }
  241. func CheckBillingResourceExpireAt(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
  242. scanExpiredBillingResources(ctx)
  243. s := auth.GetAdminSession(ctx, options.Options.Region)
  244. resp, err := notify.NotifyTopic.List(s, jsonutils.Marshal(map[string]interface{}{
  245. "filter": fmt.Sprintf("name.equals('%s')", notifyapi.DefaultResourceRelease),
  246. "scope": "system",
  247. }))
  248. if err != nil {
  249. log.Errorln(errors.Wrap(err, "list topics"))
  250. return
  251. }
  252. topics := []notifyapi.TopicDetails{}
  253. err = jsonutils.Update(&topics, resp.Data)
  254. if err != nil {
  255. log.Errorln(errors.Wrap(err, "update topic"))
  256. return
  257. }
  258. if len(topics) != 1 {
  259. log.Errorln(errors.Wrapf(errors.ErrNotSupported, "len topics :%d", len(topics)))
  260. return
  261. }
  262. for _, advanceDay := range topics[0].AdvanceDays {
  263. resources, err := BillingResourceCheckManager.GetExpiredModels(advanceDay)
  264. if err != nil {
  265. log.Errorf("unable to fetchExpiredModels: %s", err.Error())
  266. continue
  267. }
  268. for i := range resources {
  269. detailsDecro := func(ctx context.Context, details *jsonutils.JSONDict) {
  270. details.Set("advance_days", jsonutils.NewInt(int64(advanceDay)))
  271. }
  272. notifyclient.EventNotify(ctx, userCred, notifyclient.SEventNotifyParam{
  273. Obj: &resources[i],
  274. ObjDetailsDecorator: detailsDecro,
  275. Action: notifyclient.ActionExpiredRelease,
  276. AdvanceDays: advanceDay,
  277. })
  278. }
  279. }
  280. }