provider.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. package otc
  2. import (
  3. "fmt"
  4. "io"
  5. "strconv"
  6. "strings"
  7. "github.com/opencost/opencost/core/pkg/clustercache"
  8. "github.com/opencost/opencost/core/pkg/log"
  9. "github.com/opencost/opencost/core/pkg/opencost"
  10. "github.com/opencost/opencost/core/pkg/util"
  11. "github.com/opencost/opencost/pkg/cloud/models"
  12. "github.com/opencost/opencost/pkg/env"
  13. )
  14. // Kubernetes to OTC OS conversion
  15. /* Note:
  16. Kubernetes cannot fill the "kubernetes.io/os" label with the variety that OTC provides
  17. because it is based on the runtime.GOOS variable (https://kubernetes.io/docs/reference/labels-annotations-taints/#kubernetes-io-os)
  18. which can only contain some os. The pricing between everything but windows differs
  19. by about 5ct - 30ct per hour, so most os get treated like Open Linux.
  20. */
  21. var kubernetesOSTypes = map[string]string{
  22. "linux": "Open Linux",
  23. "windows": "Windows",
  24. "Open Linux": "linux",
  25. "Oracle Linux": "linux",
  26. "SUSE Linux": "linux",
  27. "SUSE for SAP": "linux",
  28. "RedHat Linux": "linux",
  29. "Windows": "windows",
  30. }
  31. // Currently assumes that no GPU is present
  32. // but aws does that too, so its fine.
  33. type otcKey struct {
  34. ProviderID string
  35. Labels map[string]string
  36. }
  37. func (k *otcKey) GPUCount() int {
  38. return 0
  39. }
  40. func (k *otcKey) GPUType() string {
  41. return ""
  42. }
  43. func (k *otcKey) ID() string {
  44. return k.ProviderID
  45. }
  46. type otcPVKey struct {
  47. RegionID string
  48. Type string
  49. Size string // in GB
  50. Labels map[string]string
  51. ProviderId string
  52. StorageClassParameters map[string]string
  53. }
  54. func (k *otcPVKey) Features() string {
  55. fmt.Printf("features for pv %s", k.ID())
  56. return k.RegionID + "," + k.Type
  57. }
  58. func (k *otcKey) Features() string {
  59. instanceType, _ := util.GetInstanceType(k.Labels)
  60. operatingSystem, _ := util.GetOperatingSystem(k.Labels)
  61. ClusterRegion, _ := util.GetRegion(k.Labels)
  62. key := ClusterRegion + "," + instanceType + "," + operatingSystem
  63. return key
  64. }
  65. // Extract/generate a key that holds the data required to calculate
  66. // the cost of the given node (like s2.large.4).
  67. func (otc *OTC) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
  68. return &otcKey{
  69. Labels: labels,
  70. ProviderID: labels["providerID"],
  71. }
  72. }
  73. // Returns the storage class for a persistent volume key.
  74. func (k *otcPVKey) GetStorageClass() string {
  75. return k.Type
  76. }
  77. // Returns the provider id for a persistent volume key.
  78. func (k *otcPVKey) ID() string {
  79. return k.ProviderId
  80. }
  81. func (otc *OTC) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
  82. providerID := ""
  83. return &otcPVKey{
  84. Labels: pv.Labels,
  85. Type: pv.Spec.StorageClassName,
  86. StorageClassParameters: parameters,
  87. RegionID: defaultRegion,
  88. ProviderId: providerID,
  89. }
  90. }
  91. /*
  92. Download the pricing data from the OTC API
  93. When a node has a specified price of e.g. 0.014 and
  94. the kubernetes node has a RAM attribute of 8232873984 Bytes.
  95. The price in Prometheus will be composed of:
  96. - the cpu/h price multiplied with the amount of VCPUs:
  97. 0.006904 * 1 => 0.006904
  98. - the RAM/h price multiplied with the amount of ram in GiB:
  99. 0.000925 * (8232873984/1024/1024/1024) => 0.0070924
  100. And the resulting node_total_hourly_price{} metric in Prometheus
  101. will approach the total node cost retrieved from OTC:
  102. ==> 0.006904 + 0.0070924 = 0.013996399999999999
  103. ~ 0.014
  104. */
  105. func (otc *OTC) DownloadPricingData() error {
  106. otc.DownloadPricingDataLock.Lock()
  107. defer otc.DownloadPricingDataLock.Unlock()
  108. // Fetch pricing data from the otc.json config in case downloading the pricing maps fails.
  109. c, err := otc.Config.GetCustomPricingData()
  110. if err != nil {
  111. log.Errorf("Error downloading default pricing data: %s", err.Error())
  112. }
  113. otc.BaseCPUPrice = c.CPU
  114. otc.BaseRAMPrice = c.RAM
  115. otc.BaseGPUPrice = c.GPU
  116. otc.clusterManagementPrice = 0.10 // TODO: What is the cluster management price?
  117. otc.projectID = c.ProjectID
  118. // Slice with all nodes currently present in the cluster.
  119. nodeList := otc.Clientset.GetAllNodes()
  120. // Slice with all storage classes.
  121. storageClasses := otc.Clientset.GetAllStorageClasses()
  122. for _, tmp := range storageClasses {
  123. fmt.Println("storage class found:")
  124. fmt.Println(tmp.Parameters)
  125. fmt.Println(tmp.Labels)
  126. fmt.Println(tmp.TypeMeta)
  127. fmt.Println(tmp.Size)
  128. }
  129. // Slice with all persistent volumes present in the cluster
  130. pvList := otc.Clientset.GetAllPersistentVolumes()
  131. // Create a slice of all existing keys in the current cluster.
  132. // (keys like "eu-de,s3.medium.1,linux" or "eu-de,s3.xlarge.2,windows")
  133. inputkeys := make(map[string]bool)
  134. tmp := []string{}
  135. for _, node := range nodeList {
  136. labels := node.Labels
  137. key := otc.GetKey(labels, node)
  138. inputkeys[key.Features()] = true
  139. tmp = append(tmp, key.Features())
  140. }
  141. for _, pv := range pvList {
  142. fmt.Println("storage class name \"" + pv.Spec.StorageClassName + "\" found")
  143. key := otc.GetPVKey(pv, map[string]string{}, "eu-de")
  144. inputkeys[key.Features()] = true
  145. tmp = append(tmp, key.Features())
  146. }
  147. otc.Pricing = make(map[string]*OTCPricing)
  148. otc.ValidPricingKeys = make(map[string]bool)
  149. products, err := otc.fetchPaginatedProducts([]string{"ecs", "ecsnoc", "memo", "uhio", "evs"})
  150. if err != nil {
  151. log.Errorf("Failed to fetch OTC pricing data: %v", err)
  152. return err
  153. }
  154. // convert the otc-reponse product-structs to opencost-compatible node structs
  155. const ClusterRegion = "eu-de"
  156. for _, product := range products {
  157. var productPricing *OTCPricing
  158. var key string
  159. // if the product is a persistent volume, it has no osUnit and no vCpu
  160. if strings.ToLower(strings.TrimSpace(product.ProductIdParameter)) == "evs" {
  161. productPricing = &OTCPricing{
  162. PVAttributes: &OTCPVAttributes{
  163. Type: product.OpiFlavour,
  164. Price: strings.Split(strings.ReplaceAll(product.PriceAmount, ",", "."), " ")[0],
  165. },
  166. }
  167. key = ClusterRegion + "," + productPricing.PVAttributes.Type
  168. } else {
  169. // else it must be a node
  170. adjustedOS := kubernetesOSTypes[product.OsUnit]
  171. productPricing = &OTCPricing{
  172. NodeAttributes: &OTCNodeAttributes{
  173. Type: product.OpiFlavour,
  174. OS: adjustedOS,
  175. Price: strings.Split(strings.ReplaceAll(product.PriceAmount, ",", "."), " ")[0],
  176. RAM: strings.Split(product.Ram, " ")[0],
  177. VCPU: product.VCpu,
  178. },
  179. }
  180. key = ClusterRegion + "," + productPricing.NodeAttributes.Type + "," + productPricing.NodeAttributes.OS
  181. }
  182. // create a key similiar to the ones created with otcKey.Features()
  183. // so that the pricing data can be fetched using an otcKey
  184. log.Info("product \"" + key + "\" found")
  185. otc.Pricing[key] = productPricing
  186. otc.ValidPricingKeys[key] = true
  187. }
  188. // debug the whole pricing
  189. log.Debugf("OTC Pricing Data: %v", otc.Pricing)
  190. // exit
  191. return nil
  192. }
  193. func (otc *OTC) NetworkPricing() (*models.Network, error) {
  194. cpricing, err := otc.Config.GetCustomPricingData()
  195. if err != nil {
  196. return nil, err
  197. }
  198. znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
  199. if err != nil {
  200. return nil, err
  201. }
  202. rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
  203. if err != nil {
  204. return nil, err
  205. }
  206. inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
  207. if err != nil {
  208. return nil, err
  209. }
  210. return &models.Network{
  211. ZoneNetworkEgressCost: znec,
  212. RegionNetworkEgressCost: rnec,
  213. InternetNetworkEgressCost: inec,
  214. }, nil
  215. }
  216. // NodePricing(Key) (*Node, PricingMetadata, error)
  217. // Read the keys features and determine the price of the Node described by
  218. // the key to construct a Pricing Node object to return and work with.
  219. func (otc *OTC) NodePricing(k models.Key) (*models.Node, models.PricingMetadata, error) {
  220. otc.DownloadPricingDataLock.RLock()
  221. defer otc.DownloadPricingDataLock.RUnlock()
  222. key := k.Features()
  223. meta := models.PricingMetadata{}
  224. log.Info("looking for pricing data of node with key features " + key)
  225. pricing, ok := otc.Pricing[key]
  226. if ok {
  227. // The pricing key was found in the pricing list of the otc provider.
  228. // Now create a pricing node from that data and return it.
  229. log.Info("pricing data found")
  230. return otc.createNode(pricing, k)
  231. } else if _, ok := otc.ValidPricingKeys[key]; ok {
  232. // The pricing key is actually valid, but somehow it could not be found.
  233. // Try re-downloading the pricing data to check for changes.
  234. log.Info("key is valid, but no associated pricing data could be found; trying to re-download pricing data")
  235. otc.DownloadPricingDataLock.RUnlock()
  236. err := otc.DownloadPricingData()
  237. otc.DownloadPricingDataLock.RLock()
  238. if err != nil {
  239. return &models.Node{
  240. Cost: otc.BaseCPUPrice,
  241. BaseCPUPrice: otc.BaseCPUPrice,
  242. BaseRAMPrice: otc.BaseRAMPrice,
  243. BaseGPUPrice: otc.BaseGPUPrice,
  244. UsesBaseCPUPrice: true,
  245. }, meta, err
  246. }
  247. pricing, ok = otc.Pricing[key]
  248. if !ok {
  249. // The given key does not exist in OTC or locally, return a default pricing node.
  250. return &models.Node{
  251. Cost: otc.BaseCPUPrice,
  252. BaseCPUPrice: otc.BaseCPUPrice,
  253. BaseRAMPrice: otc.BaseRAMPrice,
  254. BaseGPUPrice: otc.BaseGPUPrice,
  255. UsesBaseCPUPrice: true,
  256. }, meta, fmt.Errorf("unable to find any Pricing data for \"%s\"", key)
  257. }
  258. // The local pricing date was just outdated.
  259. log.Info("pricing data found after re-download")
  260. return otc.createNode(pricing, k)
  261. } else {
  262. // The given key is not valid, fall back to base pricing (handled by the costmodel)?
  263. log.Info("given key \"" + key + "\" is invalid; falling back to default pricing")
  264. return nil, meta, fmt.Errorf("invalid Pricing Key \"%s\"", key)
  265. }
  266. }
  267. // create a Pricing Node from the internal pricing struct and a key describing the kubernetes node
  268. func (otc *OTC) createNode(pricing *OTCPricing, key models.Key) (*models.Node, models.PricingMetadata, error) {
  269. // aws does some fancy stuff here, but it probably isn't that necessary
  270. // so just return the pricing node constructed directly from the internal struct
  271. meta := models.PricingMetadata{}
  272. return &models.Node{
  273. Cost: pricing.NodeAttributes.Price,
  274. VCPU: pricing.NodeAttributes.VCPU,
  275. RAM: pricing.NodeAttributes.RAM,
  276. BaseCPUPrice: otc.BaseCPUPrice,
  277. BaseRAMPrice: otc.BaseRAMPrice,
  278. BaseGPUPrice: otc.BaseGPUPrice,
  279. }, meta, nil
  280. }
  281. // give the order to read the custom provider config file
  282. func (otc *OTC) GetConfig() (*models.CustomPricing, error) {
  283. c, err := otc.Config.GetCustomPricingData()
  284. if err != nil {
  285. return nil, err
  286. }
  287. return c, nil
  288. }
  289. // load balancer cost
  290. // taken straight up from aws
  291. func (otc *OTC) LoadBalancerPricing() (*models.LoadBalancer, error) {
  292. return &models.LoadBalancer{
  293. Cost: 0.05,
  294. }, nil
  295. }
  296. // returns general info about the cluster
  297. // This method HAS to be overwritten as long as the CustomProvider
  298. // Field of the OTC struct is not set when initializing the provider
  299. // in "provider.go" (see all the other providers).
  300. func (otc *OTC) ClusterInfo() (map[string]string, error) {
  301. c, err := otc.GetConfig()
  302. if err != nil {
  303. return nil, err
  304. }
  305. m := make(map[string]string)
  306. m["name"] = "OTC Cluster #1"
  307. if clusterName := otc.getClusterName(c); clusterName != "" {
  308. m["name"] = clusterName
  309. }
  310. m["provider"] = opencost.OTCProvider
  311. m["account"] = c.ProjectID
  312. m["region"] = otc.ClusterRegion
  313. m["remoteReadEnabled"] = strconv.FormatBool(env.IsRemoteEnabled())
  314. m["id"] = env.GetClusterID()
  315. return m, nil
  316. }
  317. func (otc *OTC) getClusterName(cfg *models.CustomPricing) string {
  318. if cfg.ClusterName != "" {
  319. return cfg.ClusterName
  320. }
  321. for _, node := range otc.Clientset.GetAllNodes() {
  322. if clusterName, ok := node.Labels["name"]; ok {
  323. return clusterName
  324. }
  325. }
  326. return ""
  327. }
  328. // search for pricing data matching the given persistent volume key
  329. // in the provider's pricing list and return it
  330. func (otc *OTC) PVPricing(pvk models.PVKey) (*models.PV, error) {
  331. pricing, ok := otc.Pricing[pvk.Features()]
  332. log.Info("looking for persistent volume pricing for features \"" + pvk.Features() + "\"")
  333. if !ok {
  334. log.Info("Persistent Volume pricing not found for features \"" + pvk.Features() + "\"")
  335. log.Info("continuing with pricing for \"eu-de,vss.ssd\"")
  336. pricing, ok = otc.Pricing["eu-de,vss.ssd"]
  337. if !ok {
  338. log.Errorf("something went wrong, the DownloadPricing method probably didn't execute correctly")
  339. return &models.PV{}, nil
  340. }
  341. }
  342. // otc pv pricing is in the format: price per GB per month
  343. // this convertes that to: GB price per hour
  344. hourly, err := strconv.ParseFloat(pricing.PVAttributes.Price, 32)
  345. if err != nil {
  346. return &models.PV{}, err
  347. }
  348. hourly = hourly / 730
  349. return &models.PV{
  350. Cost: fmt.Sprintf("%v", hourly),
  351. Class: pricing.PVAttributes.Type,
  352. }, nil
  353. }
  354. // TODO: Implement method
  355. func (otc *OTC) GetAddresses() ([]byte, error) {
  356. return []byte{}, nil
  357. }
  358. // TODO: Implement method
  359. func (otc *OTC) GetDisks() ([]byte, error) {
  360. return []byte{}, nil
  361. }
  362. // TODO: Implement method
  363. func (otc *OTC) GetOrphanedResources() ([]models.OrphanedResource, error) {
  364. return []models.OrphanedResource{}, nil
  365. }
  366. // TODO: Implement method
  367. func (otc *OTC) GpuPricing(nodeLabels map[string]string) (string, error) {
  368. return "", nil
  369. }
  370. // TODO: Implement method
  371. func (otc *OTC) AllNodePricing() (interface{}, error) {
  372. return nil, nil
  373. }
  374. // TODO: Implement method
  375. func (otc *OTC) UpdateConfig(r io.Reader, updateType string) (*models.CustomPricing, error) {
  376. return &models.CustomPricing{}, nil
  377. }
  378. // TODO: Implement method
  379. func (otc *OTC) UpdateConfigFromConfigMap(configMap map[string]string) (*models.CustomPricing, error) {
  380. return &models.CustomPricing{}, nil
  381. }
  382. // TODO: Implement method
  383. func (otc *OTC) GetManagementPlatform() (string, error) {
  384. return "", nil
  385. }
  386. // TODO: Implement method
  387. func (otc *OTC) ApplyReservedInstancePricing(nodes map[string]*models.Node) {
  388. }
  389. func (otc *OTC) ServiceAccountStatus() *models.ServiceAccountStatus {
  390. return &models.ServiceAccountStatus{
  391. Checks: []*models.ServiceAccountCheck{},
  392. }
  393. }
  394. // TODO: Implement method
  395. func (otc *OTC) PricingSourceStatus() map[string]*models.PricingSource {
  396. return map[string]*models.PricingSource{}
  397. }
  398. // TODO: Implement method
  399. func (otc *OTC) ClusterManagementPricing() (string, float64, error) {
  400. return "", 0.0, nil
  401. }
  402. func (otc *OTC) CombinedDiscountForNode(nodeType string, reservedInstance bool, defaultDiscount, negotiatedDiscount float64) float64 {
  403. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  404. }
  405. // Regions retrieved from https://www.open-telekom-cloud.com/de/business-navigator/hochverfuegbare-rechenzentren
  406. var otcRegions = []string{
  407. "eu-de",
  408. "eu-nl",
  409. }
  410. func (otc *OTC) Regions() []string {
  411. regionOverrides := env.GetRegionOverrideList()
  412. if len(regionOverrides) > 0 {
  413. log.Debugf("Overriding OTC regions with configured region list: %+v", regionOverrides)
  414. return regionOverrides
  415. }
  416. return otcRegions
  417. }
  418. // PricingSourceSummary returns the pricing source summary for the provider.
  419. // The summary represents what was _parsed_ from the pricing source, not what
  420. // was returned from the relevant API.
  421. func (otc *OTC) PricingSourceSummary() interface{} {
  422. // encode the pricing source summary as a JSON string
  423. return otc.Pricing
  424. }