usageapiintegration.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. package oracle
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/log"
  8. "github.com/opencost/opencost/core/pkg/opencost"
  9. "github.com/opencost/opencost/pkg/cloud"
  10. "github.com/oracle/oci-go-sdk/v65/common"
  11. "github.com/oracle/oci-go-sdk/v65/usageapi"
  12. )
  13. type UsageApiIntegration struct {
  14. UsageApiConfiguration
  15. ConnectionStatus cloud.ConnectionStatus
  16. }
  17. type usageAPIClient interface {
  18. RequestSummarizedUsages(context.Context, usageapi.RequestSummarizedUsagesRequest) (usageapi.RequestSummarizedUsagesResponse, error)
  19. }
  20. func (uai *UsageApiIntegration) GetCloudCost(start time.Time, end time.Time) (*opencost.CloudCostSetRange, error) {
  21. client, err := uai.GetUsageApiClient()
  22. if err != nil {
  23. uai.ConnectionStatus = cloud.FailedConnection
  24. return nil, fmt.Errorf("getting oracle usage api client: %s", err.Error())
  25. }
  26. return uai.getCloudCost(context.Background(), client, start, end)
  27. }
  28. func (uai *UsageApiIntegration) getCloudCost(ctx context.Context, client usageAPIClient, start time.Time, end time.Time) (*opencost.CloudCostSetRange, error) {
  29. req := usageapi.RequestSummarizedUsagesRequest{
  30. RequestSummarizedUsagesDetails: usageapi.RequestSummarizedUsagesDetails{
  31. Granularity: usageapi.RequestSummarizedUsagesDetailsGranularityDaily,
  32. GroupBy: []string{"resourceId", "service", "subscriptionId", "tenantName"},
  33. IsAggregateByTime: common.Bool(false),
  34. TimeUsageStarted: &common.SDKTime{Time: start},
  35. TimeUsageEnded: &common.SDKTime{Time: end},
  36. QueryType: usageapi.RequestSummarizedUsagesDetailsQueryTypeCost,
  37. TenantId: common.String(uai.TenancyID),
  38. },
  39. Limit: common.Int(500),
  40. }
  41. ccsr, err := opencost.NewCloudCostSetRange(start, end, opencost.AccumulateOptionDay, uai.Key())
  42. if err != nil {
  43. return nil, err
  44. }
  45. hasItems := false
  46. seenPageTokens := map[string]struct{}{}
  47. for page := 1; ; page++ {
  48. resp, err := client.RequestSummarizedUsages(ctx, req)
  49. if err != nil {
  50. uai.ConnectionStatus = cloud.FailedConnection
  51. return nil, fmt.Errorf("failed to query usage: %w", err)
  52. }
  53. log.Debugf("UsageApiIntegration[%s]: received %d usage items from page %d", uai.Key(), len(resp.Items), page)
  54. if len(resp.Items) > 0 {
  55. hasItems = true
  56. }
  57. for _, item := range resp.Items {
  58. if item.TimeUsageStarted == nil || item.TimeUsageEnded == nil {
  59. log.Warnf("UsageApiIntegration[%s]: skipping usage item without a usage window", uai.Key())
  60. continue
  61. }
  62. cc, err := uai.usageSummaryToCloudCost(item)
  63. if err != nil {
  64. return nil, err
  65. }
  66. ccsr.LoadCloudCost(cc)
  67. }
  68. if resp.OpcNextPage == nil || *resp.OpcNextPage == "" {
  69. break
  70. }
  71. if _, ok := seenPageTokens[*resp.OpcNextPage]; ok {
  72. uai.ConnectionStatus = cloud.FailedConnection
  73. return nil, fmt.Errorf("received a repeated OCI usage API page token")
  74. }
  75. seenPageTokens[*resp.OpcNextPage] = struct{}{}
  76. req.Page = resp.OpcNextPage
  77. }
  78. // Set status to missing data if every response page was empty and the status isn't already successful.
  79. if !hasItems && uai.ConnectionStatus != cloud.SuccessfulConnection {
  80. uai.ConnectionStatus = cloud.MissingData
  81. return ccsr, nil
  82. }
  83. uai.ConnectionStatus = cloud.SuccessfulConnection
  84. return ccsr, nil
  85. }
  86. func (uai *UsageApiIntegration) usageSummaryToCloudCost(item usageapi.UsageSummary) (*opencost.CloudCost, error) {
  87. resourceID := ""
  88. if item.ResourceId != nil {
  89. resourceID = *item.ResourceId
  90. }
  91. tenantName := ""
  92. if item.TenantName != nil {
  93. tenantName = *item.TenantName
  94. }
  95. subscriptionID := ""
  96. if item.SubscriptionId != nil {
  97. subscriptionID = *item.SubscriptionId
  98. }
  99. service := ""
  100. if item.Service != nil {
  101. service = *item.Service
  102. }
  103. labels := opencost.CloudCostLabels{}
  104. for _, tag := range item.Tags {
  105. if tag.Key == nil || tag.Value == nil {
  106. continue
  107. }
  108. labels[*tag.Key] = *tag.Value
  109. }
  110. listRate := 0.0
  111. if item.ListRate != nil {
  112. listRate = float64(*item.ListRate)
  113. }
  114. attributedCost, err := parseAttributedCost(item.AttributedCost)
  115. if err != nil {
  116. return nil, err
  117. }
  118. computedAmount := 0.0
  119. if item.ComputedAmount != nil {
  120. computedAmount = float64(*item.ComputedAmount)
  121. }
  122. winStart := item.TimeUsageStarted.Time
  123. winEnd := item.TimeUsageEnded.Time
  124. return &opencost.CloudCost{
  125. Properties: &opencost.CloudCostProperties{
  126. ProviderID: resourceID,
  127. Provider: opencost.OracleProvider,
  128. AccountID: uai.TenancyID,
  129. AccountName: tenantName,
  130. InvoiceEntityID: subscriptionID,
  131. RegionID: uai.Region,
  132. Service: service,
  133. Category: SelectOCICategory(service),
  134. Labels: labels,
  135. },
  136. Window: opencost.NewWindow(&winStart, &winEnd),
  137. ListCost: opencost.CostMetric{
  138. Cost: listRate,
  139. },
  140. NetCost: opencost.CostMetric{
  141. Cost: computedAmount,
  142. },
  143. AmortizedNetCost: opencost.CostMetric{
  144. Cost: attributedCost,
  145. },
  146. AmortizedCost: opencost.CostMetric{
  147. Cost: attributedCost,
  148. },
  149. InvoicedCost: opencost.CostMetric{
  150. Cost: computedAmount,
  151. },
  152. }, nil
  153. }
  154. func (uai *UsageApiIntegration) GetStatus() cloud.ConnectionStatus {
  155. // initialize status if it has not done so; this can happen if the integration is inactive
  156. if uai.ConnectionStatus.String() == "" {
  157. uai.ConnectionStatus = cloud.InitialStatus
  158. }
  159. return uai.ConnectionStatus
  160. }
  161. func (uai *UsageApiIntegration) RefreshStatus() cloud.ConnectionStatus {
  162. log.Warn("status refresh is not supported for the Oracle provider")
  163. return uai.ConnectionStatus
  164. }
  165. func parseAttributedCost(s *string) (float64, error) {
  166. if s == nil || *s == "" {
  167. return 0, nil
  168. }
  169. f, err := strconv.ParseFloat(*s, 64)
  170. if err != nil {
  171. return 0, fmt.Errorf("unable to parse float '%s': %s", *s, err.Error())
  172. }
  173. return f, nil
  174. }
  175. func SelectOCICategory(service string) string {
  176. if service == "Compute" {
  177. return opencost.ComputeCategory
  178. } else if service == "Block Storage" || service == "Object Storage" {
  179. return opencost.StorageCategory
  180. } else if service == "Load Balancer" || service == "Virtual Cloud Network" {
  181. return opencost.NetworkCategory
  182. } else {
  183. return opencost.OtherCategory
  184. }
  185. }