ratecard.go 7.4 KB

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