2
0

provider.go 15 KB

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