azureprovider.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. package cloud
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "github.com/kubecost/cost-model/pkg/clustercache"
  13. "github.com/kubecost/cost-model/pkg/env"
  14. "github.com/kubecost/cost-model/pkg/util"
  15. "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2017-09-01/skus"
  16. "github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2018-03-31/containerservice"
  17. "github.com/Azure/azure-sdk-for-go/services/preview/commerce/mgmt/2015-06-01-preview/commerce"
  18. "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
  19. "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources"
  20. "github.com/Azure/go-autorest/autorest"
  21. "github.com/Azure/go-autorest/autorest/azure"
  22. "github.com/Azure/go-autorest/autorest/azure/auth"
  23. v1 "k8s.io/api/core/v1"
  24. "k8s.io/klog"
  25. )
  26. const (
  27. AzureFilePremiumStorageClass = "premium_smb"
  28. AzureFileStandardStorageClass = "standard_smb"
  29. AzureDiskPremiumSSDStorageClass = "premium_ssd"
  30. AzureDiskStandardSSDStorageClass = "standard_ssd"
  31. AzureDiskStandardStorageClass = "standard_hdd"
  32. )
  33. var (
  34. regionCodeMappings = map[string]string{
  35. "ap": "asia",
  36. "au": "australia",
  37. "br": "brazil",
  38. "ca": "canada",
  39. "eu": "europe",
  40. "fr": "france",
  41. "in": "india",
  42. "ja": "japan",
  43. "kr": "korea",
  44. "uk": "uk",
  45. "us": "us",
  46. "za": "southafrica",
  47. }
  48. //mtBasic, _ = regexp.Compile("^BASIC.A\\d+[_Promo]*$")
  49. //mtStandardA, _ = regexp.Compile("^A\\d+[_Promo]*$")
  50. mtStandardB, _ = regexp.Compile(`^Standard_B\d+m?[_v\d]*[_Promo]*$`)
  51. mtStandardD, _ = regexp.Compile(`^Standard_D\d[_v\d]*[_Promo]*$`)
  52. mtStandardE, _ = regexp.Compile(`^Standard_E\d+i?[_v\d]*[_Promo]*$`)
  53. mtStandardF, _ = regexp.Compile(`^Standard_F\d+[_v\d]*[_Promo]*$`)
  54. mtStandardG, _ = regexp.Compile(`^Standard_G\d+[_v\d]*[_Promo]*$`)
  55. mtStandardL, _ = regexp.Compile(`^Standard_L\d+[_v\d]*[_Promo]*$`)
  56. mtStandardM, _ = regexp.Compile(`^Standard_M\d+[m|t|l]*s[_v\d]*[_Promo]*$`)
  57. mtStandardN, _ = regexp.Compile(`^Standard_N[C|D|V]\d+r?[_v\d]*[_Promo]*$`)
  58. )
  59. var loadedAzureSecret bool = false
  60. var azureSecret *AzureServiceKey = nil
  61. type regionParts []string
  62. func (r regionParts) String() string {
  63. var result string
  64. for _, p := range r {
  65. result += p
  66. }
  67. return result
  68. }
  69. func getRegions(service string, subscriptionsClient subscriptions.Client, providersClient resources.ProvidersClient, subscriptionID string) (map[string]string, error) {
  70. allLocations := make(map[string]string)
  71. supLocations := make(map[string]string)
  72. // retrieve all locations for the subscription id (some of them may not be supported by the required provider)
  73. if locations, err := subscriptionsClient.ListLocations(context.TODO(), subscriptionID); err == nil {
  74. // fill up the map: DisplayName - > Name
  75. for _, loc := range *locations.Value {
  76. allLocations[*loc.DisplayName] = *loc.Name
  77. }
  78. } else {
  79. return nil, err
  80. }
  81. // identify supported locations for the namespace and resource type
  82. const (
  83. providerNamespaceForCompute = "Microsoft.Compute"
  84. resourceTypeForCompute = "locations/vmSizes"
  85. providerNamespaceForAks = "Microsoft.ContainerService"
  86. resourceTypeForAks = "managedClusters"
  87. )
  88. switch service {
  89. case "aks":
  90. if providers, err := providersClient.Get(context.TODO(), providerNamespaceForAks, ""); err == nil {
  91. for _, pr := range *providers.ResourceTypes {
  92. if *pr.ResourceType == resourceTypeForAks {
  93. for _, displName := range *pr.Locations {
  94. if loc, ok := allLocations[displName]; ok {
  95. supLocations[loc] = displName
  96. } else {
  97. klog.V(1).Infof("unsupported cloud region %s", loc)
  98. }
  99. }
  100. break
  101. }
  102. }
  103. } else {
  104. return nil, err
  105. }
  106. return supLocations, nil
  107. default:
  108. if providers, err := providersClient.Get(context.TODO(), providerNamespaceForCompute, ""); err == nil {
  109. for _, pr := range *providers.ResourceTypes {
  110. if *pr.ResourceType == resourceTypeForCompute {
  111. for _, displName := range *pr.Locations {
  112. if loc, ok := allLocations[displName]; ok {
  113. supLocations[loc] = displName
  114. } else {
  115. klog.V(1).Infof("unsupported cloud region %s", loc)
  116. }
  117. }
  118. break
  119. }
  120. }
  121. } else {
  122. return nil, err
  123. }
  124. return supLocations, nil
  125. }
  126. }
  127. func toRegionID(meterRegion string, regions map[string]string) (string, error) {
  128. var rp regionParts = strings.Split(strings.ToLower(meterRegion), " ")
  129. regionCode := regionCodeMappings[rp[0]]
  130. lastPart := rp[len(rp)-1]
  131. var regionIds []string
  132. if _, err := strconv.Atoi(lastPart); err == nil {
  133. regionIds = []string{
  134. fmt.Sprintf("%s%s%s", regionCode, rp[1:len(rp)-1], lastPart),
  135. fmt.Sprintf("%s%s%s", rp[1:len(rp)-1], regionCode, lastPart),
  136. }
  137. } else {
  138. regionIds = []string{
  139. fmt.Sprintf("%s%s", regionCode, rp[1:]),
  140. fmt.Sprintf("%s%s", rp[1:], regionCode),
  141. }
  142. }
  143. for _, regionID := range regionIds {
  144. if checkRegionID(regionID, regions) {
  145. return regionID, nil
  146. }
  147. }
  148. return "", fmt.Errorf("Couldn't find region")
  149. }
  150. func checkRegionID(regionID string, regions map[string]string) bool {
  151. for region := range regions {
  152. if regionID == region {
  153. return true
  154. }
  155. }
  156. return false
  157. }
  158. // AzurePricing either contains a Node or PV
  159. type AzurePricing struct {
  160. Node *Node
  161. PV *PV
  162. }
  163. type Azure struct {
  164. Pricing map[string]*AzurePricing
  165. DownloadPricingDataLock sync.RWMutex
  166. Clientset clustercache.ClusterCache
  167. Config *ProviderConfig
  168. }
  169. type azureKey struct {
  170. Labels map[string]string
  171. GPULabel string
  172. GPULabelValue string
  173. }
  174. func (k *azureKey) Features() string {
  175. r, _ := util.GetRegion(k.Labels)
  176. region := strings.ToLower(r)
  177. instance, _ := util.GetInstanceType(k.Labels)
  178. usageType := "ondemand"
  179. return fmt.Sprintf("%s,%s,%s", region, instance, usageType)
  180. }
  181. func (k *azureKey) GPUType() string {
  182. if t, ok := k.Labels[k.GPULabel]; ok {
  183. return t
  184. }
  185. return ""
  186. }
  187. func (k *azureKey) ID() string {
  188. return ""
  189. }
  190. // Represents an azure app key
  191. type AzureAppKey struct {
  192. AppID string `json:"appId"`
  193. DisplayName string `json:"displayName"`
  194. Name string `json:"name"`
  195. Password string `json:"password"`
  196. Tenant string `json:"tenant"`
  197. }
  198. // Azure service key for a specific subscription
  199. type AzureServiceKey struct {
  200. SubscriptionID string `json:"subscriptionId"`
  201. ServiceKey *AzureAppKey `json:"serviceKey"`
  202. }
  203. // Validity check on service key
  204. func (ask *AzureServiceKey) IsValid() bool {
  205. return ask.SubscriptionID != "" &&
  206. ask.ServiceKey != nil &&
  207. ask.ServiceKey.AppID != "" &&
  208. ask.ServiceKey.Password != "" &&
  209. ask.ServiceKey.Tenant != ""
  210. }
  211. // Loads the azure authentication via configuration or a secret set at install time.
  212. func (az *Azure) getAzureAuth(forceReload bool, cp *CustomPricing) (subscriptionID, clientID, clientSecret, tenantID string) {
  213. // 1. Check config values first (set from frontend UI)
  214. if cp.AzureSubscriptionID != "" && cp.AzureClientID != "" && cp.AzureClientSecret != "" && cp.AzureTenantID != "" {
  215. subscriptionID = cp.AzureSubscriptionID
  216. clientID = cp.AzureClientID
  217. clientSecret = cp.AzureClientSecret
  218. tenantID = cp.AzureTenantID
  219. return
  220. }
  221. // 2. Check for secret
  222. s, _ := az.loadAzureAuthSecret(forceReload)
  223. if s != nil && s.IsValid() {
  224. subscriptionID = s.SubscriptionID
  225. clientID = s.ServiceKey.AppID
  226. clientSecret = s.ServiceKey.Password
  227. tenantID = s.ServiceKey.Tenant
  228. return
  229. }
  230. // 3. Empty values
  231. return "", "", "", ""
  232. }
  233. // Load once and cache the result (even on failure). This is an install time secret, so
  234. // we don't expect the secret to change. If it does, however, we can force reload using
  235. // the input parameter.
  236. func (az *Azure) loadAzureAuthSecret(force bool) (*AzureServiceKey, error) {
  237. if !force && loadedAzureSecret {
  238. return azureSecret, nil
  239. }
  240. loadedAzureSecret = true
  241. exists, err := util.FileExists(authSecretPath)
  242. if !exists || err != nil {
  243. return nil, fmt.Errorf("Failed to locate service account file: %s", authSecretPath)
  244. }
  245. result, err := ioutil.ReadFile(authSecretPath)
  246. if err != nil {
  247. return nil, err
  248. }
  249. var ask AzureServiceKey
  250. err = json.Unmarshal(result, &ask)
  251. if err != nil {
  252. return nil, err
  253. }
  254. azureSecret = &ask
  255. return azureSecret, nil
  256. }
  257. func (az *Azure) GetKey(labels map[string]string, n *v1.Node) Key {
  258. cfg, err := az.GetConfig()
  259. if err != nil {
  260. klog.Infof("Error loading azure custom pricing information")
  261. }
  262. // azure defaults, see https://docs.microsoft.com/en-us/azure/aks/gpu-cluster
  263. gpuLabel := "accelerator"
  264. gpuLabelValue := "nvidia"
  265. if cfg.GpuLabel != "" {
  266. gpuLabel = cfg.GpuLabel
  267. }
  268. if cfg.GpuLabelValue != "" {
  269. gpuLabelValue = cfg.GpuLabelValue
  270. }
  271. return &azureKey{
  272. Labels: labels,
  273. GPULabel: gpuLabel,
  274. GPULabelValue: gpuLabelValue,
  275. }
  276. }
  277. // CreateString builds strings effectively
  278. func createString(keys ...string) string {
  279. var b strings.Builder
  280. for _, key := range keys {
  281. b.WriteString(key)
  282. }
  283. return b.String()
  284. }
  285. func transformMachineType(subCategory string, mt []string) []string {
  286. switch {
  287. case strings.Contains(subCategory, "Basic"):
  288. return []string{createString("Basic_", mt[0])}
  289. case len(mt) == 2:
  290. return []string{createString("Standard_", mt[0]), createString("Standard_", mt[1])}
  291. default:
  292. return []string{createString("Standard_", mt[0])}
  293. }
  294. }
  295. func addSuffix(mt string, suffixes ...string) []string {
  296. result := make([]string, len(suffixes))
  297. var suffix string
  298. parts := strings.Split(mt, "_")
  299. if len(parts) > 2 {
  300. for _, p := range parts[2:] {
  301. suffix = createString(suffix, "_", p)
  302. }
  303. }
  304. for i, s := range suffixes {
  305. result[i] = createString(parts[0], "_", parts[1], s, suffix)
  306. }
  307. return result
  308. }
  309. func getMachineTypeVariants(mt string) []string {
  310. switch {
  311. case mtStandardB.MatchString(mt):
  312. return []string{createString(mt, "s")}
  313. case mtStandardD.MatchString(mt):
  314. var result []string
  315. result = append(result, addSuffix(mt, "s")[0])
  316. dsType := strings.Replace(mt, "Standard_D", "Standard_DS", -1)
  317. result = append(result, dsType)
  318. result = append(result, addSuffix(dsType, "-1", "-2", "-4", "-8")...)
  319. return result
  320. case mtStandardE.MatchString(mt):
  321. return addSuffix(mt, "s", "-2s", "-4s", "-8s", "-16s", "-32s")
  322. case mtStandardF.MatchString(mt):
  323. return addSuffix(mt, "s")
  324. case mtStandardG.MatchString(mt):
  325. var result []string
  326. gsType := strings.Replace(mt, "Standard_G", "Standard_GS", -1)
  327. result = append(result, gsType)
  328. return append(result, addSuffix(gsType, "-4", "-8", "-16")...)
  329. case mtStandardL.MatchString(mt):
  330. return addSuffix(mt, "s")
  331. case mtStandardM.MatchString(mt) && strings.HasSuffix(mt, "ms"):
  332. base := strings.TrimSuffix(mt, "ms")
  333. return addSuffix(base, "-2ms", "-4ms", "-8ms", "-16ms", "-32ms", "-64ms")
  334. case mtStandardM.MatchString(mt) && (strings.HasSuffix(mt, "ls") || strings.HasSuffix(mt, "ts")):
  335. return []string{}
  336. case mtStandardM.MatchString(mt) && strings.HasSuffix(mt, "s"):
  337. base := strings.TrimSuffix(mt, "s")
  338. return addSuffix(base, "", "m")
  339. case mtStandardN.MatchString(mt):
  340. return addSuffix(mt, "s")
  341. }
  342. return []string{}
  343. }
  344. func (az *Azure) GetManagementPlatform() (string, error) {
  345. nodes := az.Clientset.GetAllNodes()
  346. if len(nodes) > 0 {
  347. n := nodes[0]
  348. providerID := n.Spec.ProviderID
  349. if strings.Contains(providerID, "aks") {
  350. return "aks", nil
  351. }
  352. }
  353. return "", nil
  354. }
  355. // DownloadPricingData uses provided azure "best guesses" for pricing
  356. func (az *Azure) DownloadPricingData() error {
  357. az.DownloadPricingDataLock.Lock()
  358. defer az.DownloadPricingDataLock.Unlock()
  359. config, err := az.GetConfig()
  360. if err != nil {
  361. return err
  362. }
  363. // Load the service provider keys
  364. subscriptionID, clientID, clientSecret, tenantID := az.getAzureAuth(false, config)
  365. config.AzureSubscriptionID = subscriptionID
  366. config.AzureClientID = clientID
  367. config.AzureClientSecret = clientSecret
  368. config.AzureTenantID = tenantID
  369. var authorizer autorest.Authorizer
  370. if config.AzureClientID != "" && config.AzureClientSecret != "" && config.AzureTenantID != "" {
  371. credentialsConfig := auth.NewClientCredentialsConfig(config.AzureClientID, config.AzureClientSecret, config.AzureTenantID)
  372. a, err := credentialsConfig.Authorizer()
  373. if err != nil {
  374. return err
  375. }
  376. authorizer = a
  377. }
  378. if authorizer == nil {
  379. a, err := auth.NewAuthorizerFromEnvironment()
  380. authorizer = a
  381. if err != nil { // Failed to create authorizer from environment, try from file
  382. a, err := auth.NewAuthorizerFromFile(azure.PublicCloud.ResourceManagerEndpoint)
  383. if err != nil {
  384. return err
  385. }
  386. authorizer = a
  387. }
  388. }
  389. sClient := subscriptions.NewClient()
  390. sClient.Authorizer = authorizer
  391. rcClient := commerce.NewRateCardClient(config.AzureSubscriptionID)
  392. rcClient.Authorizer = authorizer
  393. skusClient := skus.NewResourceSkusClient(config.AzureSubscriptionID)
  394. skusClient.Authorizer = authorizer
  395. providersClient := resources.NewProvidersClient(config.AzureSubscriptionID)
  396. providersClient.Authorizer = authorizer
  397. containerServiceClient := containerservice.NewContainerServicesClient(config.AzureSubscriptionID)
  398. containerServiceClient.Authorizer = authorizer
  399. rateCardFilter := fmt.Sprintf("OfferDurableId eq 'MS-AZR-0003p' and Currency eq '%s' and Locale eq 'en-US' and RegionInfo eq '%s'", config.CurrencyCode, config.AzureBillingRegion)
  400. klog.Infof("Using ratecard query %s", rateCardFilter)
  401. result, err := rcClient.Get(context.TODO(), rateCardFilter)
  402. if err != nil {
  403. return err
  404. }
  405. allPrices := make(map[string]*AzurePricing)
  406. regions, err := getRegions("compute", sClient, providersClient, config.AzureSubscriptionID)
  407. if err != nil {
  408. return err
  409. }
  410. c, err := az.GetConfig()
  411. if err != nil {
  412. return err
  413. }
  414. baseCPUPrice := c.CPU
  415. for _, v := range *result.Meters {
  416. meterName := *v.MeterName
  417. meterRegion := *v.MeterRegion
  418. meterCategory := *v.MeterCategory
  419. meterSubCategory := *v.MeterSubCategory
  420. region, err := toRegionID(meterRegion, regions)
  421. if err != nil {
  422. continue
  423. }
  424. if !strings.Contains(meterSubCategory, "Windows") {
  425. if strings.Contains(meterCategory, "Storage") {
  426. if strings.Contains(meterSubCategory, "HDD") || strings.Contains(meterSubCategory, "SSD") || strings.Contains(meterSubCategory, "Premium Files") {
  427. var storageClass string = ""
  428. if strings.Contains(meterName, "P4 ") {
  429. storageClass = AzureDiskPremiumSSDStorageClass
  430. } else if strings.Contains(meterName, "E4 ") {
  431. storageClass = AzureDiskStandardSSDStorageClass
  432. } else if strings.Contains(meterName, "S4 ") {
  433. storageClass = AzureDiskStandardStorageClass
  434. } else if strings.Contains(meterName, "LRS Provisioned") {
  435. storageClass = AzureFilePremiumStorageClass
  436. }
  437. if storageClass != "" {
  438. var priceInUsd float64
  439. if len(v.MeterRates) < 1 {
  440. klog.V(1).Infof("missing rate info %+v", map[string]interface{}{"MeterSubCategory": *v.MeterSubCategory, "region": region})
  441. continue
  442. }
  443. for _, rate := range v.MeterRates {
  444. priceInUsd += *rate
  445. }
  446. // rate is in disk per month, resolve price per hour, then GB per hour
  447. pricePerHour := priceInUsd / 730.0 / 32.0
  448. priceStr := fmt.Sprintf("%f", pricePerHour)
  449. key := region + "," + storageClass
  450. klog.V(4).Infof("Adding PV.Key: %s, Cost: %s", key, priceStr)
  451. allPrices[key] = &AzurePricing{
  452. PV: &PV{
  453. Cost: priceStr,
  454. Region: region,
  455. },
  456. }
  457. }
  458. }
  459. }
  460. if strings.Contains(meterCategory, "Virtual Machines") {
  461. usageType := ""
  462. if !strings.Contains(meterName, "Low Priority") {
  463. usageType = "ondemand"
  464. } else {
  465. usageType = "preemptible"
  466. }
  467. var instanceTypes []string
  468. name := strings.TrimSuffix(meterName, " Low Priority")
  469. instanceType := strings.Split(name, "/")
  470. for _, it := range instanceType {
  471. if strings.Contains(meterSubCategory, "Promo") {
  472. it = it + " Promo"
  473. }
  474. instanceTypes = append(instanceTypes, strings.Replace(it, " ", "_", 1))
  475. }
  476. instanceTypes = transformMachineType(meterSubCategory, instanceTypes)
  477. if strings.Contains(name, "Expired") {
  478. instanceTypes = []string{}
  479. }
  480. var priceInUsd float64
  481. if len(v.MeterRates) < 1 {
  482. klog.V(1).Infof("missing rate info %+v", map[string]interface{}{"MeterSubCategory": *v.MeterSubCategory, "region": region})
  483. continue
  484. }
  485. for _, rate := range v.MeterRates {
  486. priceInUsd += *rate
  487. }
  488. priceStr := fmt.Sprintf("%f", priceInUsd)
  489. for _, instanceType := range instanceTypes {
  490. key := fmt.Sprintf("%s,%s,%s", region, instanceType, usageType)
  491. allPrices[key] = &AzurePricing{
  492. Node: &Node{
  493. Cost: priceStr,
  494. BaseCPUPrice: baseCPUPrice,
  495. },
  496. }
  497. }
  498. }
  499. }
  500. }
  501. // There is no easy way of supporting Standard Azure-File, because it's billed per used GB
  502. // this will set the price to "0" as a workaround to not spam with `Persistent Volume pricing not found for` error
  503. // check https://github.com/kubecost/cost-model/issues/159 for more information (same problem on AWS)
  504. zeroPrice := "0.0"
  505. for region := range regions {
  506. key := region + "," + AzureFileStandardStorageClass
  507. klog.V(4).Infof("Adding PV.Key: %s, Cost: %s", key, zeroPrice)
  508. allPrices[key] = &AzurePricing{
  509. PV: &PV{
  510. Cost: zeroPrice,
  511. Region: region,
  512. },
  513. }
  514. }
  515. az.Pricing = allPrices
  516. return nil
  517. }
  518. // AllNodePricing returns the Azure pricing objects stored
  519. func (az *Azure) AllNodePricing() (interface{}, error) {
  520. az.DownloadPricingDataLock.RLock()
  521. defer az.DownloadPricingDataLock.RUnlock()
  522. return az.Pricing, nil
  523. }
  524. // NodePricing returns Azure pricing data for a single node
  525. func (az *Azure) NodePricing(key Key) (*Node, error) {
  526. az.DownloadPricingDataLock.RLock()
  527. defer az.DownloadPricingDataLock.RUnlock()
  528. if n, ok := az.Pricing[key.Features()]; ok {
  529. klog.V(4).Infof("Returning pricing for node %s: %+v from key %s", key, n, key.Features())
  530. if key.GPUType() != "" {
  531. n.Node.GPU = "1" // TODO: support multiple GPUs
  532. }
  533. return n.Node, nil
  534. }
  535. klog.V(1).Infof("[Warning] no pricing data found for %s: %s", key.Features(), key)
  536. c, err := az.GetConfig()
  537. if err != nil {
  538. return nil, fmt.Errorf("No default pricing data available")
  539. }
  540. if key.GPUType() != "" {
  541. return &Node{
  542. VCPUCost: c.CPU,
  543. RAMCost: c.RAM,
  544. GPUCost: c.GPU,
  545. GPU: "1", // TODO: support multiple GPUs
  546. }, nil
  547. }
  548. return &Node{
  549. VCPUCost: c.CPU,
  550. RAMCost: c.RAM,
  551. UsesBaseCPUPrice: true,
  552. }, nil
  553. }
  554. // Stubbed NetworkPricing for Azure. Pull directly from azure.json for now
  555. func (az *Azure) NetworkPricing() (*Network, error) {
  556. cpricing, err := az.Config.GetCustomPricingData()
  557. if err != nil {
  558. return nil, err
  559. }
  560. znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
  561. if err != nil {
  562. return nil, err
  563. }
  564. rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
  565. if err != nil {
  566. return nil, err
  567. }
  568. inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
  569. if err != nil {
  570. return nil, err
  571. }
  572. return &Network{
  573. ZoneNetworkEgressCost: znec,
  574. RegionNetworkEgressCost: rnec,
  575. InternetNetworkEgressCost: inec,
  576. }, nil
  577. }
  578. func (azr *Azure) LoadBalancerPricing() (*LoadBalancer, error) {
  579. fffrc := 0.025
  580. afrc := 0.010
  581. lbidc := 0.008
  582. numForwardingRules := 1.0
  583. dataIngressGB := 0.0
  584. var totalCost float64
  585. if numForwardingRules < 5 {
  586. totalCost = fffrc*numForwardingRules + lbidc*dataIngressGB
  587. } else {
  588. totalCost = fffrc*5 + afrc*(numForwardingRules-5) + lbidc*dataIngressGB
  589. }
  590. return &LoadBalancer{
  591. Cost: totalCost,
  592. }, nil
  593. }
  594. type azurePvKey struct {
  595. Labels map[string]string
  596. StorageClass string
  597. StorageClassParameters map[string]string
  598. DefaultRegion string
  599. }
  600. func (az *Azure) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
  601. return &azurePvKey{
  602. Labels: pv.Labels,
  603. StorageClass: pv.Spec.StorageClassName,
  604. StorageClassParameters: parameters,
  605. DefaultRegion: defaultRegion,
  606. }
  607. }
  608. func (key *azurePvKey) ID() string {
  609. return ""
  610. }
  611. func (key *azurePvKey) GetStorageClass() string {
  612. return key.StorageClass
  613. }
  614. func (key *azurePvKey) Features() string {
  615. storageClass := key.StorageClassParameters["storageaccounttype"]
  616. storageSKU := key.StorageClassParameters["skuName"]
  617. if storageClass != "" {
  618. if strings.EqualFold(storageClass, "Premium_LRS") {
  619. storageClass = AzureDiskPremiumSSDStorageClass
  620. } else if strings.EqualFold(storageClass, "StandardSSD_LRS") {
  621. storageClass = AzureDiskStandardSSDStorageClass
  622. } else if strings.EqualFold(storageClass, "Standard_LRS") {
  623. storageClass = AzureDiskStandardStorageClass
  624. }
  625. } else {
  626. if strings.EqualFold(storageSKU, "Premium_LRS") {
  627. storageClass = AzureFilePremiumStorageClass
  628. } else if strings.EqualFold(storageSKU, "Standard_LRS") {
  629. storageClass = AzureFileStandardStorageClass
  630. }
  631. }
  632. if region, ok := util.GetRegion(key.Labels); ok {
  633. return region + "," + storageClass
  634. }
  635. return key.DefaultRegion + "," + storageClass
  636. }
  637. func (*Azure) GetAddresses() ([]byte, error) {
  638. return nil, nil
  639. }
  640. func (*Azure) GetDisks() ([]byte, error) {
  641. return nil, nil
  642. }
  643. func (az *Azure) ClusterInfo() (map[string]string, error) {
  644. remoteEnabled := env.IsRemoteEnabled()
  645. m := make(map[string]string)
  646. m["name"] = "Azure Cluster #1"
  647. c, err := az.GetConfig()
  648. if err != nil {
  649. return nil, err
  650. }
  651. if c.ClusterName != "" {
  652. m["name"] = c.ClusterName
  653. }
  654. m["provider"] = "azure"
  655. m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
  656. m["id"] = env.GetClusterID()
  657. return m, nil
  658. }
  659. func (az *Azure) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
  660. return az.Config.UpdateFromMap(a)
  661. }
  662. func (az *Azure) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  663. defer az.DownloadPricingData()
  664. return az.Config.Update(func(c *CustomPricing) error {
  665. a := make(map[string]interface{})
  666. err := json.NewDecoder(r).Decode(&a)
  667. if err != nil {
  668. return err
  669. }
  670. for k, v := range a {
  671. kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  672. vstr, ok := v.(string)
  673. if ok {
  674. err := SetCustomPricingField(c, kUpper, vstr)
  675. if err != nil {
  676. return err
  677. }
  678. } else {
  679. sci := v.(map[string]interface{})
  680. sc := make(map[string]string)
  681. for k, val := range sci {
  682. sc[k] = val.(string)
  683. }
  684. c.SharedCosts = sc //todo: support reflection/multiple map fields
  685. }
  686. }
  687. if env.IsRemoteEnabled() {
  688. err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
  689. if err != nil {
  690. return err
  691. }
  692. }
  693. return nil
  694. })
  695. }
  696. func (az *Azure) GetConfig() (*CustomPricing, error) {
  697. c, err := az.Config.GetCustomPricingData()
  698. if c.Discount == "" {
  699. c.Discount = "0%"
  700. }
  701. if c.NegotiatedDiscount == "" {
  702. c.NegotiatedDiscount = "0%"
  703. }
  704. if c.CurrencyCode == "" {
  705. c.CurrencyCode = "USD"
  706. }
  707. if c.AzureBillingRegion == "" {
  708. c.AzureBillingRegion = "US"
  709. }
  710. if err != nil {
  711. return nil, err
  712. }
  713. return c, nil
  714. }
  715. func (az *Azure) ExternalAllocations(string, string, []string, string, string, bool) ([]*OutOfClusterAllocation, error) {
  716. return nil, nil
  717. }
  718. func (az *Azure) ApplyReservedInstancePricing(nodes map[string]*Node) {
  719. }
  720. func (az *Azure) PVPricing(pvk PVKey) (*PV, error) {
  721. az.DownloadPricingDataLock.RLock()
  722. defer az.DownloadPricingDataLock.RUnlock()
  723. pricing, ok := az.Pricing[pvk.Features()]
  724. if !ok {
  725. klog.V(4).Infof("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
  726. return &PV{}, nil
  727. }
  728. return pricing.PV, nil
  729. }
  730. func (az *Azure) GetLocalStorageQuery(window, offset string, rate bool, used bool) string {
  731. return ""
  732. }
  733. func (az *Azure) ServiceAccountStatus() *ServiceAccountStatus {
  734. return &ServiceAccountStatus{
  735. Checks: []*ServiceAccountCheck{},
  736. }
  737. }
  738. func (az *Azure) PricingSourceStatus() map[string]*PricingSource {
  739. return make(map[string]*PricingSource)
  740. }
  741. func (*Azure) ClusterManagementPricing() (string, float64, error) {
  742. return "", 0.0, nil
  743. }
  744. func (az *Azure) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
  745. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  746. }
  747. func (az *Azure) ParseID(id string) string {
  748. return id
  749. }
  750. func (az *Azure) ParsePVID(id string) string {
  751. return id
  752. }