ratecard.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package oracle
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "github.com/opencost/opencost/pkg/cloud/httputil"
  10. "github.com/opencost/opencost/pkg/cloud/models"
  11. )
  12. // 1 month = 744 hours by price list documentation.
  13. const hoursPerMonth = 24 * 31
  14. type RateCardStore struct {
  15. shapesEndpoint string
  16. url string
  17. currencyCode string
  18. client *http.Client
  19. prices map[string]Price
  20. }
  21. type Price struct {
  22. ProductName string
  23. Metric string
  24. Model string
  25. UnitPrice float64
  26. }
  27. type PricingResponse struct {
  28. Items []Item `json:"items"`
  29. }
  30. type Item struct {
  31. PartNumber string `json:"partNumber"`
  32. DisplayName string `json:"displayName"`
  33. MetricName string `json:"metricName"`
  34. ServiceCategory string `json:"serviceCategory"`
  35. CurrencyCodeLocalizations []struct {
  36. CurrencyCode string `json:"currencyCode"`
  37. Prices []struct {
  38. Model string `json:"model"`
  39. Value float64 `json:"value"`
  40. } `json:"prices"`
  41. } `json:"currencyCodeLocalizations"`
  42. Description string `json:"description,omitempty"`
  43. }
  44. func NewRateCardStore(url, currencyCode string) *RateCardStore {
  45. return &RateCardStore{
  46. url: url,
  47. currencyCode: currencyCode,
  48. // Zero-value http.Client has no timeout; use the shared bounded client so
  49. // a stalled rate-card endpoint can't hang ingestion.
  50. client: httputil.BoundedClient(),
  51. prices: map[string]Price{},
  52. }
  53. }
  54. func (rcs *RateCardStore) ForLB(defaultPricing DefaultPricing) (*models.LoadBalancer, error) {
  55. var cost float64
  56. rc, ok := rcs.prices[loadBalancerPartNumber]
  57. if ok {
  58. cost = rc.UnitPrice
  59. } else {
  60. c, err := strconv.ParseFloat(defaultPricing.LB, 64)
  61. if err != nil {
  62. return nil, err
  63. }
  64. cost = c
  65. }
  66. return &models.LoadBalancer{
  67. Cost: cost,
  68. }, nil
  69. }
  70. func (rcs *RateCardStore) ForManagedCluster(clusterType string) float64 {
  71. // Basic clusters are free, and do not have a rate card.
  72. if clusterType == "BASIC_CLUSTER" {
  73. return 0.0
  74. }
  75. // Enhanced clusters have a rate card, and require a pricing look up.
  76. rc, ok := rcs.prices[enhancedClusterPartNumber]
  77. if !ok {
  78. return 0.1
  79. }
  80. return rc.UnitPrice
  81. }
  82. func (rcs *RateCardStore) ForEgressRegion(region string, defaultPricing DefaultPricing) (*models.Network, error) {
  83. pn := egressRegionPartNumber(region)
  84. var egressCost float64
  85. if rc, ok := rcs.prices[pn]; ok {
  86. egressCost = rc.UnitPrice
  87. } else if defaultPricing.Egress != "" {
  88. cost, err := strconv.ParseFloat(defaultPricing.Egress, 64)
  89. if err != nil {
  90. return nil, err
  91. }
  92. egressCost = cost
  93. }
  94. return &models.Network{
  95. ZoneNetworkEgressCost: 0,
  96. RegionNetworkEgressCost: egressCost,
  97. InternetNetworkEgressCost: egressCost,
  98. NatGatewayEgressCost: 0,
  99. NatGatewayIngressCost: 0,
  100. }, nil
  101. }
  102. // ForPVK retrieves a Gb/Hour cost for a given PVKey.
  103. func (rcs *RateCardStore) ForPVK(pvk models.PVKey, defaultPricing DefaultPricing) (*models.PV, error) {
  104. features := pvk.Features()
  105. rc, ok := rcs.prices[features]
  106. if !ok {
  107. // Use default storage if no pricing found
  108. return &models.PV{
  109. Cost: defaultPricing.Storage,
  110. }, nil
  111. }
  112. return &models.PV{
  113. Cost: fmt.Sprintf("%f", rc.UnitPrice/hoursPerMonth), // Oracle unit pricing for storage is in Gb/Month
  114. }, nil
  115. }
  116. // ForKey retrieves costing metadata for a key.
  117. func (rcs *RateCardStore) ForKey(key models.Key, defaultPricing DefaultPricing) (*models.Node, models.PricingMetadata, error) {
  118. features := strings.Split(key.Features(), ",")
  119. shape := features[0]
  120. cpuPriceMultiplier := 1.0
  121. product := instanceProducts.get(shape)
  122. if baseShape, multiplier, ok := normalizeOCIInstanceShape(shape); ok {
  123. baseProduct := instanceProducts.get(baseShape)
  124. if !baseProduct.isEmpty() {
  125. shape = baseShape
  126. cpuPriceMultiplier = multiplier
  127. product = baseProduct
  128. }
  129. }
  130. var node *models.Node
  131. // Use the default pricing if the instance product is unknown
  132. if product.isEmpty() {
  133. totalCost, err := defaultPricing.TotalInstanceCost()
  134. if err != nil {
  135. return nil, models.PricingMetadata{}, fmt.Errorf("failed to parse default Oracle pricing: %w", err)
  136. }
  137. vcpuCost := defaultPricing.OCPU
  138. if !isARMArch(features) {
  139. // Non-ARM architectures have 2 VCPU per OCPU
  140. vcpuFloat, err := strconv.ParseFloat(vcpuCost, 64)
  141. if err != nil {
  142. return nil, models.PricingMetadata{}, err
  143. }
  144. vcpuFloat /= 2
  145. vcpuCost = fmt.Sprintf("%f", vcpuFloat)
  146. }
  147. node = &models.Node{
  148. Cost: fmt.Sprintf("%f", totalCost),
  149. VCPUCost: vcpuCost,
  150. RAM: defaultPricing.Memory,
  151. GPU: defaultPricing.GPU,
  152. }
  153. } else {
  154. ocpuPrice := rcs.prices[product.OCPU].UnitPrice * cpuPriceMultiplier
  155. if !isARMArch(features) {
  156. // Non-ARM architectures have 2 VCPU per OCPU
  157. ocpuPrice /= 2
  158. }
  159. memoryPrice := rcs.prices[product.Memory].UnitPrice
  160. gpuPrice := rcs.prices[product.GPU].UnitPrice
  161. diskPrice := rcs.prices[product.Disk].UnitPrice
  162. // convert disk price from Tb/hour to Gb/hour
  163. diskPrice /= 1000
  164. totalPrice := diskPrice + ocpuPrice + memoryPrice + gpuPrice
  165. // Add virtual node pricing if it is being used.
  166. if len(features) > 1 && features[1] == "true" {
  167. totalPrice += rcs.prices[virualNodePartNumber].UnitPrice
  168. }
  169. node = &models.Node{
  170. Cost: fmt.Sprintf("%f", totalPrice),
  171. StorageCost: fmt.Sprintf("%f", diskPrice),
  172. VCPUCost: fmt.Sprintf("%f", ocpuPrice),
  173. RAMCost: fmt.Sprintf("%f", memoryPrice),
  174. GPUCost: fmt.Sprintf("%f", gpuPrice),
  175. }
  176. }
  177. return node, models.PricingMetadata{}, nil
  178. }
  179. func (rcs *RateCardStore) Store() map[string]Price {
  180. return rcs.prices
  181. }
  182. func (rcs *RateCardStore) Refresh() (map[string]Price, error) {
  183. if err := rcs.refresh(); err != nil {
  184. return nil, err
  185. }
  186. return rcs.prices, nil
  187. }
  188. // refresh the prices of Price information
  189. func (rcs *RateCardStore) refresh() error {
  190. rcr, err := rcs.getProductPricing()
  191. if err != nil {
  192. return err
  193. }
  194. rcs.prices = rcr.toStore()
  195. return nil
  196. }
  197. func (rcs *RateCardStore) getProductPricing() (*PricingResponse, error) {
  198. url := fmt.Sprintf("%s?currencyCode=%s", rcs.url, rcs.currencyCode)
  199. prBytes, err := rcs.loadMetadata(url)
  200. if err != nil {
  201. return nil, err
  202. }
  203. pr := &PricingResponse{}
  204. if err := json.Unmarshal(prBytes, pr); err != nil {
  205. return nil, err
  206. }
  207. return pr, nil
  208. }
  209. func (rcs *RateCardStore) loadMetadata(url string) ([]byte, error) {
  210. req, err := http.NewRequest("GET", url, nil)
  211. if err != nil {
  212. return nil, err
  213. }
  214. resp, err := rcs.client.Do(req)
  215. if err != nil {
  216. return nil, err
  217. }
  218. defer resp.Body.Close()
  219. if resp.StatusCode != http.StatusOK {
  220. return nil, fmt.Errorf("failed to get pricing metadata data, got code %d", resp.StatusCode)
  221. }
  222. return io.ReadAll(resp.Body)
  223. }
  224. func (rcr *PricingResponse) toStore() map[string]Price {
  225. store := map[string]Price{}
  226. for _, item := range rcr.Items {
  227. store[item.PartNumber] = item.toRateCard()
  228. }
  229. return store
  230. }
  231. func (i Item) hasPrice() bool {
  232. if len(i.CurrencyCodeLocalizations) < 1 {
  233. return false
  234. }
  235. return len(i.CurrencyCodeLocalizations[0].Prices) > 0
  236. }
  237. func (i Item) toRateCard() Price {
  238. var unitPrice float64
  239. var model string
  240. if i.hasPrice() {
  241. for _, price := range i.CurrencyCodeLocalizations[0].Prices {
  242. // Some products have range pricing, we'll take the first non-zero pricing in this case.
  243. if price.Value > 0 {
  244. unitPrice = price.Value
  245. model = price.Model
  246. }
  247. }
  248. }
  249. return Price{
  250. ProductName: i.DisplayName,
  251. Metric: i.MetricName,
  252. UnitPrice: unitPrice,
  253. Model: model,
  254. }
  255. }
  256. func (p Product) isEmpty() bool {
  257. return p.GPU == "" && p.OCPU == "" && p.Memory == "" && p.Disk == ""
  258. }
  259. func isARMArch(features []string) bool {
  260. if len(features) < 3 {
  261. return false
  262. }
  263. return strings.HasPrefix(strings.ToLower(features[2]), "arm")
  264. }