business.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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 azure
  15. import (
  16. "fmt"
  17. "sort"
  18. "strings"
  19. "time"
  20. "yunion.io/x/pkg/errors"
  21. )
  22. type SMonthBill struct {
  23. Month string `json:"month"`
  24. StartDate string `json:"start_date"`
  25. EndDate string `json:"end_date"`
  26. Currency string `json:"currency"`
  27. Total float64 `json:"total"`
  28. Metric string `json:"metric"`
  29. Granularity string `json:"granularity"`
  30. Subscriptions []SMonthBillServiceFee `json:"subscriptions"`
  31. }
  32. type SMonthBillServiceFee struct {
  33. SubscriptionId string `json:"subscription_id"`
  34. Amount float64 `json:"amount"`
  35. Unit string `json:"unit"`
  36. }
  37. type SCostQueryResult struct {
  38. Properties struct {
  39. Columns []struct {
  40. Name string `json:"name"`
  41. Type string `json:"type"`
  42. } `json:"columns"`
  43. Rows [][]interface{} `json:"rows"`
  44. } `json:"properties"`
  45. }
  46. func getMonthDateRange(month string) (string, string, error) {
  47. monthStart, err := time.Parse("2006-01", month)
  48. if err != nil {
  49. return "", "", errors.Wrapf(err, "invalid month %q, expected YYYY-MM", month)
  50. }
  51. monthEnd := monthStart.AddDate(0, 1, -1)
  52. return monthStart.Format("2006-01-02"), monthEnd.Format("2006-01-02"), nil
  53. }
  54. func (self *SAzureClient) GetMonthBill(month string) (*SMonthBill, error) {
  55. startDate, endDate, err := getMonthDateRange(month)
  56. if err != nil {
  57. return nil, err
  58. }
  59. billingAccounts, err := self.GetBillingAccounts()
  60. if err != nil {
  61. return nil, errors.Wrap(err, "GetBillingAccounts")
  62. }
  63. if len(billingAccounts) == 0 {
  64. return nil, errors.Errorf("no available billing accounts")
  65. }
  66. ret := &SMonthBill{
  67. Month: month,
  68. StartDate: startDate,
  69. EndDate: endDate,
  70. Metric: "PreTaxCost",
  71. Granularity: "monthly",
  72. Subscriptions: make([]SMonthBillServiceFee, 0),
  73. }
  74. feeBySubscriptionId := map[string]float64{}
  75. for i := range billingAccounts {
  76. accountPath := strings.TrimSpace(billingAccounts[i].Id)
  77. if len(accountPath) == 0 && len(billingAccounts[i].Name) > 0 {
  78. accountPath = fmt.Sprintf("/providers/Microsoft.Billing/billingAccounts/%s", billingAccounts[i].Name)
  79. }
  80. if len(accountPath) == 0 {
  81. continue
  82. }
  83. resource := fmt.Sprintf("%s/providers/Microsoft.CostManagement/query", strings.TrimSuffix(accountPath, "/"))
  84. resp, err := self.post_v2(resource, "2025-03-01", map[string]interface{}{
  85. "type": "ActualCost",
  86. "timeframe": "Custom",
  87. "timePeriod": map[string]string{
  88. "from": fmt.Sprintf("%s", startDate),
  89. "to": fmt.Sprintf("%s", endDate),
  90. },
  91. "dataset": map[string]interface{}{
  92. "granularity": "None",
  93. "aggregation": map[string]interface{}{
  94. "totalCost": map[string]string{
  95. "name": "PreTaxCost",
  96. "function": "Sum",
  97. },
  98. },
  99. "grouping": []map[string]string{
  100. {
  101. "type": "Dimension",
  102. "name": "SubscriptionId",
  103. },
  104. },
  105. },
  106. })
  107. if err != nil {
  108. return nil, errors.Wrapf(err, "post_v2 cost query for billing account %s", billingAccounts[i].Name)
  109. }
  110. queryRet := SCostQueryResult{}
  111. if err := resp.Unmarshal(&queryRet); err != nil {
  112. return nil, errors.Wrapf(err, "resp.Unmarshal cost query for billing account %s", billingAccounts[i].Name)
  113. }
  114. subscriptionIdx, costIdx, currencyIdx := -1, -1, -1
  115. for c := range queryRet.Properties.Columns {
  116. name := strings.ToLower(queryRet.Properties.Columns[c].Name)
  117. switch name {
  118. case "subscriptionid", "subscriptionname":
  119. subscriptionIdx = c
  120. case "totalcost", "pretaxcost":
  121. costIdx = c
  122. case "currency":
  123. currencyIdx = c
  124. }
  125. }
  126. if subscriptionIdx < 0 || costIdx < 0 {
  127. continue
  128. }
  129. for j := range queryRet.Properties.Rows {
  130. row := queryRet.Properties.Rows[j]
  131. if len(row) <= costIdx || len(row) <= subscriptionIdx {
  132. continue
  133. }
  134. subscription := strings.TrimSpace(fmt.Sprintf("%v", row[subscriptionIdx]))
  135. if len(subscription) == 0 {
  136. subscription = "Unknown"
  137. }
  138. cost, ok := row[costIdx].(float64)
  139. if !ok {
  140. continue
  141. }
  142. feeBySubscriptionId[subscription] += cost
  143. ret.Total += cost
  144. if len(ret.Currency) == 0 {
  145. if currencyIdx >= 0 && len(row) > currencyIdx {
  146. ret.Currency = strings.TrimSpace(fmt.Sprintf("%v", row[currencyIdx]))
  147. }
  148. }
  149. }
  150. }
  151. for subscriptionId, amount := range feeBySubscriptionId {
  152. ret.Subscriptions = append(ret.Subscriptions, SMonthBillServiceFee{
  153. SubscriptionId: subscriptionId,
  154. Amount: amount,
  155. Unit: ret.Currency,
  156. })
  157. }
  158. sort.Slice(ret.Subscriptions, func(i, j int) bool {
  159. return ret.Subscriptions[i].Amount > ret.Subscriptions[j].Amount
  160. })
  161. return ret, nil
  162. }