history.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package usages
  15. import (
  16. "context"
  17. "fmt"
  18. "net/http"
  19. "time"
  20. "yunion.io/x/jsonutils"
  21. "yunion.io/x/log"
  22. "yunion.io/x/pkg/errors"
  23. "yunion.io/x/pkg/util/rbacscope"
  24. "yunion.io/x/sqlchemy"
  25. api "yunion.io/x/onecloud/pkg/apis/compute"
  26. "yunion.io/x/onecloud/pkg/appsrv"
  27. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  28. "yunion.io/x/onecloud/pkg/cloudcommon/policy"
  29. "yunion.io/x/onecloud/pkg/compute/models"
  30. "yunion.io/x/onecloud/pkg/httperrors"
  31. "yunion.io/x/onecloud/pkg/mcclient"
  32. "yunion.io/x/onecloud/pkg/mcclient/auth"
  33. "yunion.io/x/onecloud/pkg/util/hashcache"
  34. "yunion.io/x/onecloud/pkg/util/rbacutils"
  35. )
  36. var (
  37. historyUsageCache = hashcache.NewCache(1024, time.Second*300) // 5 minutes, 1024 buckets cache
  38. )
  39. type TimeRange struct {
  40. StartDate time.Time `json:"start_date"`
  41. EndDate time.Time `json:"end_date"`
  42. Interval string `json:"interval"`
  43. }
  44. func (self *TimeRange) ValidateInput() error {
  45. if self.StartDate.IsZero() {
  46. return httperrors.NewMissingParameterError("start_date")
  47. }
  48. if self.EndDate.IsZero() {
  49. return httperrors.NewMissingParameterError("end_date")
  50. }
  51. if len(self.Interval) == 0 {
  52. return httperrors.NewMissingParameterError("interval")
  53. }
  54. if self.StartDate.After(self.EndDate) {
  55. return httperrors.NewInputParameterError("start_date should befor end_date")
  56. }
  57. switch self.Interval {
  58. case "hour":
  59. if self.EndDate.Sub(self.StartDate) > time.Hour*72 {
  60. return httperrors.NewOutOfRangeError("The time interval exceeds 72 hours")
  61. }
  62. case "day":
  63. if self.EndDate.Sub(self.StartDate) > time.Hour*24*31 {
  64. return httperrors.NewOutOfRangeError("The time interval exceeds 31 days")
  65. }
  66. case "month":
  67. if self.EndDate.Sub(self.StartDate) > time.Hour*24*365 {
  68. return httperrors.NewOutOfRangeError("The time interval exceeds 1 year")
  69. }
  70. case "year":
  71. if self.EndDate.Sub(self.StartDate) > time.Hour*24*365*20 {
  72. return httperrors.NewOutOfRangeError("The time interval exceeds 20 year")
  73. }
  74. default:
  75. return httperrors.NewInputParameterError("invalid interval %s", self.Interval)
  76. }
  77. return nil
  78. }
  79. func getHistoryCacheKey(
  80. scope rbacscope.TRbacScope,
  81. userCred mcclient.IIdentityProvider,
  82. timeRange *TimeRange,
  83. includeSystem bool,
  84. policyResult rbacutils.SPolicyResult,
  85. ) string {
  86. type KeyStruct struct {
  87. Scope rbacscope.TRbacScope `json:"scope"`
  88. Domain string `json:"domain"`
  89. Project string `json:"project"`
  90. System bool `json:"system"`
  91. TimeRange *TimeRange `json:"time_range"`
  92. PolicyResult rbacutils.SPolicyResult `json:"policy_result"`
  93. }
  94. key := KeyStruct{}
  95. key.Scope = scope
  96. switch scope {
  97. case rbacscope.ScopeSystem:
  98. case rbacscope.ScopeDomain:
  99. key.Domain = userCred.GetProjectDomainId()
  100. case rbacscope.ScopeProject:
  101. key.Project = userCred.GetProjectId()
  102. }
  103. key.TimeRange = timeRange
  104. key.System = includeSystem
  105. key.PolicyResult = policyResult
  106. jsonObj := jsonutils.Marshal(key)
  107. return jsonObj.QueryString()
  108. }
  109. func AddHistoryUsageHandler(prefix string, app *appsrv.Application) {
  110. prefix = fmt.Sprintf("%s/history-usages", prefix)
  111. for key, f := range map[string]appsrv.FilterHandler{
  112. "": historyRangeObjHandler(nil, ReportGeneralHistoryUsage),
  113. } {
  114. addHistoryHandler(prefix, key, f, app)
  115. }
  116. }
  117. func addHistoryHandler(prefix, rangeObjKey string, hf appsrv.FilterHandler, app *appsrv.Application) {
  118. ahf := auth.Authenticate(hf)
  119. name := "get_history_usage"
  120. if len(rangeObjKey) != 0 {
  121. prefix = fmt.Sprintf("%s/%ss/<id>", prefix, rangeObjKey)
  122. name = fmt.Sprintf("get_%s_history_usage", rangeObjKey)
  123. }
  124. app.AddHandler2("GET", prefix, ahf, nil, name, nil)
  125. }
  126. type objHistoryUsageFunc func(context.Context, mcclient.TokenCredential, rbacscope.TRbacScope, mcclient.IIdentityProvider, *TimeRange, bool, rbacutils.SPolicyResult) (Usage, error)
  127. func historyRangeObjHandler(
  128. manager db.IStandaloneModelManager,
  129. reporter objHistoryUsageFunc,
  130. ) appsrv.FilterHandler {
  131. return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  132. userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
  133. ownerId, scope, err, result := db.FetchUsageOwnerScope(ctx, userCred, getQuery(r))
  134. if err != nil {
  135. httperrors.GeneralServerError(ctx, w, err)
  136. return
  137. }
  138. query := getQuery(r)
  139. tags := rbacutils.SPolicyResult{Result: rbacutils.Allow}
  140. query.Unmarshal(&tags)
  141. result = result.Merge(tags)
  142. log.Debugf("ownerId: %s scope: %s result: %s", ownerId, scope, result.String())
  143. timeRange := &TimeRange{}
  144. query.Unmarshal(timeRange)
  145. err = timeRange.ValidateInput()
  146. if err != nil {
  147. httperrors.GeneralServerError(ctx, w, err)
  148. return
  149. }
  150. includeSystem := jsonutils.QueryBoolean(query, "system", false)
  151. refresh := jsonutils.QueryBoolean(query, "refresh", false)
  152. key := getHistoryCacheKey(scope, ownerId, timeRange, includeSystem, result)
  153. if !refresh {
  154. cached := historyUsageCache.Get(key)
  155. if cached != nil {
  156. response(w, "history-usage", cached)
  157. return
  158. }
  159. }
  160. usage, err := reporter(ctx, userCred, scope, ownerId, timeRange, includeSystem, result)
  161. if err != nil {
  162. httperrors.GeneralServerError(ctx, w, err)
  163. return
  164. }
  165. historyUsageCache.AtomicSet(key, usage)
  166. response(w, "history-usage", usage)
  167. }
  168. }
  169. func ReportGeneralHistoryUsage(
  170. ctx context.Context,
  171. userToken mcclient.TokenCredential,
  172. scope rbacscope.TRbacScope,
  173. userCred mcclient.IIdentityProvider,
  174. timeRange *TimeRange,
  175. includeSystem bool,
  176. policyResult rbacutils.SPolicyResult,
  177. ) (count Usage, err error) {
  178. count = make(map[string]interface{})
  179. if scope == rbacscope.ScopeSystem {
  180. count = HistoryUsage(ctx, userToken, timeRange, rbacscope.ScopeSystem, userCred, includeSystem, policyResult)
  181. }
  182. if scope == rbacscope.ScopeDomain && len(userCred.GetProjectDomainId()) > 0 {
  183. count = HistoryUsage(ctx, userToken, timeRange, rbacscope.ScopeDomain, userCred, includeSystem, policyResult)
  184. }
  185. if scope == rbacscope.ScopeProject && len(userCred.GetProjectId()) > 0 {
  186. count = HistoryUsage(ctx, userToken, timeRange, rbacscope.ScopeProject, userCred, includeSystem, policyResult)
  187. }
  188. return
  189. }
  190. func HistoryUsage(ctx context.Context, userCred mcclient.TokenCredential, timeRange *TimeRange, scope rbacscope.TRbacScope, ownerId mcclient.IIdentityProvider, includeSystem bool, policyResult rbacutils.SPolicyResult) Usage {
  191. count := make(map[string]interface{})
  192. results := db.UsagePolicyCheck(userCred, models.GuestManager, scope)
  193. results = results.Merge(policyResult)
  194. if results.Result.IsDeny() {
  195. // deny
  196. return count
  197. }
  198. format := ""
  199. switch timeRange.Interval {
  200. case "hour":
  201. format = "%Y-%m-%d %h-00-00"
  202. case "day":
  203. format = "%Y-%m-%d"
  204. case "month":
  205. format = "%Y-%m"
  206. case "year":
  207. format = "%Y"
  208. default:
  209. return count
  210. }
  211. for _, manager := range []db.IModelManager{
  212. models.GuestManager,
  213. models.ElasticipManager,
  214. models.DBInstanceManager,
  215. models.LoadbalancerManager,
  216. models.HostManager,
  217. models.VpcManager,
  218. models.ElasticcacheManager,
  219. models.CloudaccountManager,
  220. models.DiskManager,
  221. models.DnsZoneManager,
  222. models.KubeClusterManager,
  223. models.NatGatewayManager,
  224. models.BucketManager,
  225. models.MongoDBManager,
  226. } {
  227. usage, _ := historyUsage(ctx, manager, scope, ownerId, timeRange, format, includeSystem, false, results)
  228. count[manager.Keyword()] = usage
  229. }
  230. usage, _ := historyUsage(ctx, models.HostManager, scope, ownerId, timeRange, format, includeSystem, true, results)
  231. count["baremetal"] = usage
  232. return count
  233. }
  234. type SHistoryUsage struct {
  235. Date string
  236. Count int
  237. }
  238. func historyUsage(
  239. ctx context.Context,
  240. manager db.IModelManager,
  241. scope rbacscope.TRbacScope,
  242. ownerId mcclient.IIdentityProvider,
  243. timeRange *TimeRange,
  244. format string,
  245. includeSystem bool,
  246. isBaremetal bool,
  247. policyResult rbacutils.SPolicyResult,
  248. ) ([]SHistoryUsage, error) {
  249. ret := []SHistoryUsage{}
  250. date, _start := []string{}, timeRange.StartDate
  251. for _start.Before(timeRange.EndDate) {
  252. switch timeRange.Interval {
  253. case "hour":
  254. date = append(date, _start.Format("2006-01-02 15:00:00"))
  255. _start = _start.Add(time.Hour)
  256. case "day":
  257. date = append(date, _start.Format("2006-01-02"))
  258. _start = _start.AddDate(0, 0, 1)
  259. case "month":
  260. date = append(date, _start.Format("2006-01"))
  261. _start = _start.AddDate(0, 1, 0)
  262. case "year":
  263. date = append(date, _start.Format("2006"))
  264. _start = _start.AddDate(1, 0, 0)
  265. }
  266. }
  267. dqs := []sqlchemy.IQuery{}
  268. for _, d := range date {
  269. dsq := manager.Query().SubQuery()
  270. dq := dsq.Query(
  271. sqlchemy.NewConstField(d).Label("date"),
  272. )
  273. dqs = append(dqs, dq)
  274. }
  275. dsq := sqlchemy.Union(dqs...).Query().SubQuery()
  276. gq := manager.Query()
  277. if manager.Keyword() == models.HostManager.Keyword() {
  278. if isBaremetal {
  279. gq = gq.Filter(sqlchemy.AND(
  280. sqlchemy.IsTrue(gq.Field("is_baremetal")),
  281. sqlchemy.Equals(gq.Field("host_type"), api.HOST_TYPE_BAREMETAL),
  282. ))
  283. } else {
  284. gq = gq.Filter(sqlchemy.OR(
  285. sqlchemy.IsFalse(gq.Field("is_baremetal")),
  286. sqlchemy.NotEquals(gq.Field("host_type"), api.HOST_TYPE_BAREMETAL),
  287. ))
  288. }
  289. }
  290. switch scope {
  291. case rbacscope.ScopeSystem:
  292. case rbacscope.ScopeDomain:
  293. gq = gq.Filter(sqlchemy.Equals(gq.Field("domain_id"), ownerId.GetProjectDomainId()))
  294. case rbacscope.ScopeProject:
  295. gq = gq.Filter(sqlchemy.Equals(gq.Field("tenant_id"), ownerId.GetProjectId()))
  296. }
  297. gq = db.ObjectIdQueryWithPolicyResult(ctx, gq, manager, policyResult)
  298. if _, ok := manager.(db.IVirtualModelManager); ok && !includeSystem {
  299. gq = gq.Filter(sqlchemy.OR(
  300. sqlchemy.IsNull(gq.Field("is_system")), sqlchemy.IsFalse(gq.Field("is_system"))))
  301. }
  302. hq := gq.Copy().LT("created_at", timeRange.StartDate)
  303. hcnt, err := hq.CountWithError()
  304. if err != nil {
  305. return nil, errors.Wrapf(err, "CountWithError")
  306. }
  307. gq = gq.GE("created_at", timeRange.StartDate).LE("created_at", timeRange.EndDate)
  308. mq := gq.SubQuery()
  309. sq := mq.Query(
  310. sqlchemy.DATE_FORMAT("date", mq.Field("created_at"), format),
  311. sqlchemy.COUNT("count", mq.Field("id")),
  312. ).GroupBy("date")
  313. msq := sq.SubQuery()
  314. q := dsq.Query(
  315. dsq.Field("date"),
  316. msq.Field("count"),
  317. ).LeftJoin(msq, sqlchemy.Equals(msq.Field("date"), dsq.Field("date"))).Asc(dsq.Field("date"))
  318. err = q.All(&ret)
  319. if err != nil {
  320. return nil, err
  321. }
  322. prev := hcnt
  323. for i := range ret {
  324. ret[i].Count = ret[i].Count + prev
  325. prev = ret[i].Count
  326. }
  327. return ret, err
  328. }