ratecard.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. }, nil
  96. }
  97. // ForPVK retrieves a Gb/Hour cost for a given PVKey.
  98. func (rcs *RateCardStore) ForPVK(pvk models.PVKey, defaultPricing DefaultPricing) (*models.PV, error) {
  99. features := pvk.Features()
  100. rc, ok := rcs.prices[features]
  101. if !ok {
  102. // Use default storage if no pricing found
  103. return &models.PV{
  104. Cost: defaultPricing.Storage,
  105. }, nil
  106. }
  107. return &models.PV{
  108. Cost: fmt.Sprintf("%f", rc.UnitPrice/hoursPerMonth), // Oracle unit pricing for storage is in Gb/Month
  109. }, nil
  110. }
  111. // ForKey retrieves costing metadata for a key.
  112. func (rcs *RateCardStore) ForKey(key models.Key, defaultPricing DefaultPricing) (*models.Node, models.PricingMetadata, error) {
  113. features := strings.Split(key.Features(), ",")
  114. product := instanceProducts.get(features[0])
  115. var node *models.Node
  116. // Use the default pricing if the instance product is unknown
  117. if product.isEmpty() {
  118. totalCost, err := defaultPricing.TotalInstanceCost()
  119. if err != nil {
  120. return nil, models.PricingMetadata{}, fmt.Errorf("failed to parse default Oracle pricing: %w", err)
  121. }
  122. vcpuCost := defaultPricing.OCPU
  123. if !isARMArch(features) {
  124. // Non-ARM architectures have 2 VCPU per OCPU
  125. vcpuFloat, err := strconv.ParseFloat(vcpuCost, 64)
  126. if err != nil {
  127. return nil, models.PricingMetadata{}, err
  128. }
  129. vcpuFloat /= 2
  130. vcpuCost = fmt.Sprintf("%f", vcpuFloat)
  131. }
  132. node = &models.Node{
  133. Cost: fmt.Sprintf("%f", totalCost),
  134. VCPUCost: vcpuCost,
  135. RAM: defaultPricing.Memory,
  136. GPU: defaultPricing.GPU,
  137. }
  138. } else {
  139. ocpuPrice := rcs.prices[product.OCPU].UnitPrice
  140. if !isARMArch(features) {
  141. // Non-ARM architectures have 2 VCPU per OCPU
  142. ocpuPrice /= 2
  143. }
  144. memoryPrice := rcs.prices[product.Memory].UnitPrice
  145. gpuPrice := rcs.prices[product.GPU].UnitPrice
  146. diskPrice := rcs.prices[product.Disk].UnitPrice
  147. // convert disk price from Tb/hour to Gb/hour
  148. diskPrice /= 1000
  149. totalPrice := diskPrice + ocpuPrice + memoryPrice + gpuPrice
  150. // Add virtual node pricing if it is being used.
  151. if len(features) > 1 && features[1] == "true" {
  152. totalPrice += rcs.prices[virualNodePartNumber].UnitPrice
  153. }
  154. node = &models.Node{
  155. Cost: fmt.Sprintf("%f", totalPrice),
  156. StorageCost: fmt.Sprintf("%f", diskPrice),
  157. VCPUCost: fmt.Sprintf("%f", ocpuPrice),
  158. RAMCost: fmt.Sprintf("%f", memoryPrice),
  159. GPUCost: fmt.Sprintf("%f", gpuPrice),
  160. }
  161. }
  162. return node, models.PricingMetadata{}, nil
  163. }
  164. func (rcs *RateCardStore) Store() map[string]Price {
  165. return rcs.prices
  166. }
  167. func (rcs *RateCardStore) Refresh() (map[string]Price, error) {
  168. if err := rcs.refresh(); err != nil {
  169. return nil, err
  170. }
  171. return rcs.prices, nil
  172. }
  173. // refresh the prices of Price information
  174. func (rcs *RateCardStore) refresh() error {
  175. rcr, err := rcs.getProductPricing()
  176. if err != nil {
  177. return err
  178. }
  179. rcs.prices = rcr.toStore()
  180. return nil
  181. }
  182. func (rcs *RateCardStore) getProductPricing() (*PricingResponse, error) {
  183. url := fmt.Sprintf("%s?currencyCode=%s", rcs.url, rcs.currencyCode)
  184. prBytes, err := rcs.loadMetadata(url)
  185. if err != nil {
  186. return nil, err
  187. }
  188. pr := &PricingResponse{}
  189. if err := json.Unmarshal(prBytes, pr); err != nil {
  190. return nil, err
  191. }
  192. return pr, nil
  193. }
  194. func (rcs *RateCardStore) loadMetadata(url string) ([]byte, error) {
  195. req, err := http.NewRequest("GET", url, nil)
  196. if err != nil {
  197. return nil, err
  198. }
  199. resp, err := rcs.client.Do(req)
  200. if err != nil {
  201. return nil, err
  202. }
  203. defer resp.Body.Close()
  204. if resp.StatusCode != http.StatusOK {
  205. return nil, fmt.Errorf("failed to get pricing metadata data, got code %d", resp.StatusCode)
  206. }
  207. return io.ReadAll(resp.Body)
  208. }
  209. func (rcr *PricingResponse) toStore() map[string]Price {
  210. store := map[string]Price{}
  211. for _, item := range rcr.Items {
  212. store[item.PartNumber] = item.toRateCard()
  213. }
  214. return store
  215. }
  216. func (i Item) hasPrice() bool {
  217. if len(i.CurrencyCodeLocalizations) < 1 {
  218. return false
  219. }
  220. return len(i.CurrencyCodeLocalizations[0].Prices) > 0
  221. }
  222. func (i Item) toRateCard() Price {
  223. var unitPrice float64
  224. var model string
  225. if i.hasPrice() {
  226. for _, price := range i.CurrencyCodeLocalizations[0].Prices {
  227. // Some products have range pricing, we'll take the first non-zero pricing in this case.
  228. if price.Value > 0 {
  229. unitPrice = price.Value
  230. model = price.Model
  231. }
  232. }
  233. }
  234. return Price{
  235. ProductName: i.DisplayName,
  236. Metric: i.MetricName,
  237. UnitPrice: unitPrice,
  238. Model: model,
  239. }
  240. }
  241. func (p Product) isEmpty() bool {
  242. return p.GPU == "" && p.OCPU == "" && p.Memory == "" && p.Disk == ""
  243. }
  244. func isARMArch(features []string) bool {
  245. if len(features) < 3 {
  246. return false
  247. }
  248. return strings.HasPrefix(strings.ToLower(features[2]), "arm")
  249. }