azureprovider.go 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528
  1. package cloud
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "github.com/opencost/opencost/pkg/kubecost"
  15. "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-11-01/compute"
  16. "github.com/Azure/azure-sdk-for-go/services/preview/commerce/mgmt/2015-06-01-preview/commerce"
  17. "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
  18. "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources"
  19. "github.com/Azure/go-autorest/autorest"
  20. "github.com/Azure/go-autorest/autorest/azure"
  21. "github.com/Azure/go-autorest/autorest/azure/auth"
  22. "github.com/opencost/opencost/pkg/clustercache"
  23. "github.com/opencost/opencost/pkg/env"
  24. "github.com/opencost/opencost/pkg/log"
  25. "github.com/opencost/opencost/pkg/util"
  26. "github.com/opencost/opencost/pkg/util/fileutil"
  27. "github.com/opencost/opencost/pkg/util/json"
  28. "github.com/opencost/opencost/pkg/util/timeutil"
  29. v1 "k8s.io/api/core/v1"
  30. )
  31. const (
  32. AzureFilePremiumStorageClass = "premium_smb"
  33. AzureFileStandardStorageClass = "standard_smb"
  34. AzureDiskPremiumSSDStorageClass = "premium_ssd"
  35. AzureDiskStandardSSDStorageClass = "standard_ssd"
  36. AzureDiskStandardStorageClass = "standard_hdd"
  37. defaultSpotLabel = "kubernetes.azure.com/scalesetpriority"
  38. defaultSpotLabelValue = "spot"
  39. AzureStorageUpdateType = "AzureStorage"
  40. )
  41. var (
  42. regionCodeMappings = map[string]string{
  43. "ap": "asia",
  44. "au": "australia",
  45. "br": "brazil",
  46. "ca": "canada",
  47. "eu": "europe",
  48. "fr": "france",
  49. "in": "india",
  50. "ja": "japan",
  51. "kr": "korea",
  52. "uk": "uk",
  53. "us": "us",
  54. "za": "southafrica",
  55. "no": "norway",
  56. "ch": "switzerland",
  57. "de": "germany",
  58. "ue": "uae",
  59. }
  60. //mtBasic, _ = regexp.Compile("^BASIC.A\\d+[_Promo]*$")
  61. //mtStandardA, _ = regexp.Compile("^A\\d+[_Promo]*$")
  62. mtStandardB, _ = regexp.Compile(`^Standard_B\d+m?[_v\d]*[_Promo]*$`)
  63. mtStandardD, _ = regexp.Compile(`^Standard_D\d[_v\d]*[_Promo]*$`)
  64. mtStandardE, _ = regexp.Compile(`^Standard_E\d+i?[_v\d]*[_Promo]*$`)
  65. mtStandardF, _ = regexp.Compile(`^Standard_F\d+[_v\d]*[_Promo]*$`)
  66. mtStandardG, _ = regexp.Compile(`^Standard_G\d+[_v\d]*[_Promo]*$`)
  67. mtStandardL, _ = regexp.Compile(`^Standard_L\d+[_v\d]*[_Promo]*$`)
  68. mtStandardM, _ = regexp.Compile(`^Standard_M\d+[m|t|l]*s[_v\d]*[_Promo]*$`)
  69. mtStandardN, _ = regexp.Compile(`^Standard_N[C|D|V]\d+r?[_v\d]*[_Promo]*$`)
  70. // azure:///subscriptions/0badafdf-1234-abcd-wxyz-123456789/...
  71. // => 0badafdf-1234-abcd-wxyz-123456789
  72. azureSubRegex = regexp.MustCompile("azure:///subscriptions/([^/]*)/*")
  73. )
  74. // List obtained by installing the Azure CLI tool "az", described here:
  75. // https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt
  76. // logging into an Azure account, and running command `az account list-locations`
  77. var azureRegions = []string{
  78. "eastus",
  79. "eastus2",
  80. "southcentralus",
  81. "westus2",
  82. "westus3",
  83. "australiaeast",
  84. "southeastasia",
  85. "northeurope",
  86. "swedencentral",
  87. "uksouth",
  88. "westeurope",
  89. "centralus",
  90. "northcentralus",
  91. "westus",
  92. "southafricanorth",
  93. "centralindia",
  94. "eastasia",
  95. "japaneast",
  96. "jioindiawest",
  97. "koreacentral",
  98. "canadacentral",
  99. "francecentral",
  100. "germanywestcentral",
  101. "norwayeast",
  102. "switzerlandnorth",
  103. "uaenorth",
  104. "brazilsouth",
  105. "centralusstage",
  106. "eastusstage",
  107. "eastus2stage",
  108. "northcentralusstage",
  109. "southcentralusstage",
  110. "westusstage",
  111. "westus2stage",
  112. "asia",
  113. "asiapacific",
  114. "australia",
  115. "brazil",
  116. "canada",
  117. "europe",
  118. "france",
  119. "germany",
  120. "global",
  121. "india",
  122. "japan",
  123. "korea",
  124. "norway",
  125. "southafrica",
  126. "switzerland",
  127. "uae",
  128. "uk",
  129. "unitedstates",
  130. "eastasiastage",
  131. "southeastasiastage",
  132. "centraluseuap",
  133. "eastus2euap",
  134. "westcentralus",
  135. "southafricawest",
  136. "australiacentral",
  137. "australiacentral2",
  138. "australiasoutheast",
  139. "japanwest",
  140. "jioindiacentral",
  141. "koreasouth",
  142. "southindia",
  143. "westindia",
  144. "canadaeast",
  145. "francesouth",
  146. "germanynorth",
  147. "norwaywest",
  148. "switzerlandwest",
  149. "ukwest",
  150. "uaecentral",
  151. "brazilsoutheast",
  152. "usgovarizona",
  153. "usgoviowa",
  154. "usgovvirginia",
  155. "usgovtexas",
  156. }
  157. type regionParts []string
  158. func (r regionParts) String() string {
  159. var result string
  160. for _, p := range r {
  161. result += p
  162. }
  163. return result
  164. }
  165. func getRegions(service string, subscriptionsClient subscriptions.Client, providersClient resources.ProvidersClient, subscriptionID string) (map[string]string, error) {
  166. allLocations := make(map[string]string)
  167. supLocations := make(map[string]string)
  168. // retrieve all locations for the subscription id (some of them may not be supported by the required provider)
  169. if locations, err := subscriptionsClient.ListLocations(context.TODO(), subscriptionID); err == nil {
  170. // fill up the map: DisplayName - > Name
  171. for _, loc := range *locations.Value {
  172. allLocations[*loc.DisplayName] = *loc.Name
  173. }
  174. } else {
  175. return nil, err
  176. }
  177. // identify supported locations for the namespace and resource type
  178. const (
  179. providerNamespaceForCompute = "Microsoft.Compute"
  180. resourceTypeForCompute = "locations/vmSizes"
  181. providerNamespaceForAks = "Microsoft.ContainerService"
  182. resourceTypeForAks = "managedClusters"
  183. )
  184. switch service {
  185. case "aks":
  186. if providers, err := providersClient.Get(context.TODO(), providerNamespaceForAks, ""); err == nil {
  187. for _, pr := range *providers.ResourceTypes {
  188. if *pr.ResourceType == resourceTypeForAks {
  189. for _, displName := range *pr.Locations {
  190. if loc, ok := allLocations[displName]; ok {
  191. supLocations[loc] = displName
  192. } else {
  193. log.Warnf("unsupported cloud region %s", loc)
  194. }
  195. }
  196. break
  197. }
  198. }
  199. } else {
  200. return nil, err
  201. }
  202. return supLocations, nil
  203. default:
  204. if providers, err := providersClient.Get(context.TODO(), providerNamespaceForCompute, ""); err == nil {
  205. for _, pr := range *providers.ResourceTypes {
  206. if *pr.ResourceType == resourceTypeForCompute {
  207. for _, displName := range *pr.Locations {
  208. if loc, ok := allLocations[displName]; ok {
  209. supLocations[loc] = displName
  210. } else {
  211. log.Warnf("unsupported cloud region %s", loc)
  212. }
  213. }
  214. break
  215. }
  216. }
  217. } else {
  218. return nil, err
  219. }
  220. return supLocations, nil
  221. }
  222. }
  223. func getRetailPrice(region string, skuName string, currencyCode string, spot bool) (string, error) {
  224. pricingURL := "https://prices.azure.com/api/retail/prices?$skip=0"
  225. if currencyCode != "" {
  226. pricingURL += fmt.Sprintf("&currencyCode='%s'", currencyCode)
  227. }
  228. var filterParams []string
  229. if region != "" {
  230. regionParam := fmt.Sprintf("armRegionName eq '%s'", region)
  231. filterParams = append(filterParams, regionParam)
  232. }
  233. if skuName != "" {
  234. skuNameParam := fmt.Sprintf("armSkuName eq '%s'", skuName)
  235. filterParams = append(filterParams, skuNameParam)
  236. }
  237. if len(filterParams) > 0 {
  238. filterParamsEscaped := url.QueryEscape(strings.Join(filterParams[:], " and "))
  239. pricingURL += fmt.Sprintf("&$filter=%s", filterParamsEscaped)
  240. }
  241. log.Infof("starting download retail price payload from \"%s\"", pricingURL)
  242. resp, err := http.Get(pricingURL)
  243. if err != nil {
  244. return "", fmt.Errorf("bogus fetch of \"%s\": %v", pricingURL, err)
  245. }
  246. if resp.StatusCode < 200 && resp.StatusCode > 299 {
  247. return "", fmt.Errorf("retail price responded with error status code %d", resp.StatusCode)
  248. }
  249. pricingPayload := AzureRetailPricing{}
  250. body, err := io.ReadAll(resp.Body)
  251. if err != nil {
  252. return "", fmt.Errorf("Error getting response: %v", err)
  253. }
  254. jsonErr := json.Unmarshal(body, &pricingPayload)
  255. if jsonErr != nil {
  256. return "", fmt.Errorf("Error unmarshalling data: %v", jsonErr)
  257. }
  258. retailPrice := ""
  259. for _, item := range pricingPayload.Items {
  260. if item.Type == "Consumption" && !strings.Contains(item.ProductName, "Windows") {
  261. // if spot is true SkuName should contain "spot, if it is false it should not
  262. if spot == strings.Contains(strings.ToLower(item.SkuName), " spot") {
  263. retailPrice = fmt.Sprintf("%f", item.RetailPrice)
  264. }
  265. }
  266. }
  267. log.DedupedInfof(5, "done parsing retail price payload from \"%s\"\n", pricingURL)
  268. if retailPrice == "" {
  269. return retailPrice, fmt.Errorf("Couldn't find price for product \"%s\" in \"%s\" region", skuName, region)
  270. }
  271. return retailPrice, nil
  272. }
  273. func toRegionID(meterRegion string, regions map[string]string) (string, error) {
  274. var rp regionParts = strings.Split(strings.ToLower(meterRegion), " ")
  275. regionCode := regionCodeMappings[rp[0]]
  276. lastPart := rp[len(rp)-1]
  277. var regionIds []string
  278. if regionID, ok := regionIdByDisplayName[meterRegion]; ok {
  279. regionIds = []string{
  280. regionID,
  281. }
  282. } else if _, err := strconv.Atoi(lastPart); err == nil {
  283. regionIds = []string{
  284. fmt.Sprintf("%s%s%s", regionCode, rp[1:len(rp)-1], lastPart),
  285. fmt.Sprintf("%s%s%s", rp[1:len(rp)-1], regionCode, lastPart),
  286. }
  287. } else {
  288. regionIds = []string{
  289. fmt.Sprintf("%s%s", regionCode, rp[1:]),
  290. fmt.Sprintf("%s%s", rp[1:], regionCode),
  291. }
  292. }
  293. for _, regionID := range regionIds {
  294. if checkRegionID(regionID, regions) {
  295. return regionID, nil
  296. }
  297. }
  298. return "", fmt.Errorf("Couldn't find region")
  299. }
  300. // azure has very inconsistent naming standards between display names from the rate card api and display names from the regions api
  301. // this map is to connect display names from the ratecard api to the appropriate id.
  302. var regionIdByDisplayName = map[string]string{
  303. "US Gov AZ": "usgovarizona",
  304. "US Gov TX": "usgovtexas",
  305. "US Gov": "usgovvirginia",
  306. }
  307. func checkRegionID(regionID string, regions map[string]string) bool {
  308. for region := range regions {
  309. if regionID == region {
  310. return true
  311. }
  312. }
  313. return false
  314. }
  315. // AzureRetailPricing struct for unmarshalling Azure Retail pricing api JSON response
  316. type AzureRetailPricing struct {
  317. BillingCurrency string `json:"BillingCurrency"`
  318. CustomerEntityId string `json:"CustomerEntityId"`
  319. CustomerEntityType string `json:"CustomerEntityType"`
  320. Items []AzureRetailPricingAttributes `json:"Items"`
  321. NextPageLink string `json:"NextPageLink"`
  322. Count int `json:"Count"`
  323. }
  324. // AzureRetailPricingAttributes struct for unmarshalling Azure Retail pricing api JSON response
  325. type AzureRetailPricingAttributes struct {
  326. CurrencyCode string `json:"currencyCode"`
  327. TierMinimumUnits float32 `json:"tierMinimumUnits"`
  328. RetailPrice float32 `json:"retailPrice"`
  329. UnitPrice float32 `json:"unitPrice"`
  330. ArmRegionName string `json:"armRegionName"`
  331. Location string `json:"location"`
  332. EffectiveStartDate *time.Time `json:"effectiveStartDate"`
  333. EffectiveEndDate *time.Time `json:"effectiveEndDate"`
  334. MeterId string `json:"meterId"`
  335. MeterName string `json:"meterName"`
  336. ProductId string `json:"productId"`
  337. SkuId string `json:"skuId"`
  338. ProductName string `json:"productName"`
  339. SkuName string `json:"skuName"`
  340. ServiceName string `json:"serviceName"`
  341. ServiceId string `json:"serviceId"`
  342. ServiceFamily string `json:"serviceFamily"`
  343. UnitOfMeasure string `json:"unitOfMeasure"`
  344. Type string `json:"type"`
  345. IsPrimaryMeterRegion bool `json:"isPrimaryMeterRegion"`
  346. ArmSkuName string `json:"armSkuName"`
  347. }
  348. // AzurePricing either contains a Node or PV
  349. type AzurePricing struct {
  350. Node *Node
  351. PV *PV
  352. }
  353. type Azure struct {
  354. Pricing map[string]*AzurePricing
  355. DownloadPricingDataLock sync.RWMutex
  356. Clientset clustercache.ClusterCache
  357. Config *ProviderConfig
  358. serviceAccountChecks *ServiceAccountChecks
  359. RateCardPricingError error
  360. clusterAccountId string
  361. clusterRegion string
  362. loadedAzureSecret bool
  363. azureSecret *AzureServiceKey
  364. loadedAzureStorageConfigSecret bool
  365. azureStorageConfig *AzureStorageConfig
  366. }
  367. // PricingSourceSummary returns the pricing source summary for the provider.
  368. // The summary represents what was _parsed_ from the pricing source, not
  369. // everything that was _available_ in the pricing source.
  370. func (az *Azure) PricingSourceSummary() interface{} {
  371. return az.Pricing
  372. }
  373. type azureKey struct {
  374. Labels map[string]string
  375. GPULabel string
  376. GPULabelValue string
  377. }
  378. func (k *azureKey) Features() string {
  379. r, _ := util.GetRegion(k.Labels)
  380. region := strings.ToLower(r)
  381. instance, _ := util.GetInstanceType(k.Labels)
  382. usageType := "ondemand"
  383. return fmt.Sprintf("%s,%s,%s", region, instance, usageType)
  384. }
  385. func (k *azureKey) GPUCount() int {
  386. return 0
  387. }
  388. // GPUType returns value of GPULabel if present
  389. func (k *azureKey) GPUType() string {
  390. if t, ok := k.Labels[k.GPULabel]; ok {
  391. return t
  392. }
  393. return ""
  394. }
  395. func (k *azureKey) isValidGPUNode() bool {
  396. return k.GPUType() == k.GPULabelValue && k.GetGPUCount() != "0"
  397. }
  398. func (k *azureKey) ID() string {
  399. return ""
  400. }
  401. func (k *azureKey) GetGPUCount() string {
  402. instance, _ := util.GetInstanceType(k.Labels)
  403. // Double digits that could get matches lower in logic
  404. if strings.Contains(instance, "NC64") {
  405. return "4"
  406. }
  407. if strings.Contains(instance, "ND96") ||
  408. strings.Contains(instance, "ND40") {
  409. return "8"
  410. }
  411. // Ordered asc because of some series have different gpu counts on different versions
  412. if strings.Contains(instance, "NC6") ||
  413. strings.Contains(instance, "NC4") ||
  414. strings.Contains(instance, "NC8") ||
  415. strings.Contains(instance, "NC16") ||
  416. strings.Contains(instance, "ND6") ||
  417. strings.Contains(instance, "NV12s") ||
  418. strings.Contains(instance, "NV6") {
  419. return "1"
  420. }
  421. if strings.Contains(instance, "NC12") ||
  422. strings.Contains(instance, "ND12") ||
  423. strings.Contains(instance, "NV24s") ||
  424. strings.Contains(instance, "NV12") {
  425. return "2"
  426. }
  427. if strings.Contains(instance, "NC24") ||
  428. strings.Contains(instance, "ND24") ||
  429. strings.Contains(instance, "NV48s") ||
  430. strings.Contains(instance, "NV24") {
  431. return "4"
  432. }
  433. return "0"
  434. }
  435. // AzureStorageConfig Represents an azure storage config
  436. type AzureStorageConfig struct {
  437. SubscriptionId string `json:"azureSubscriptionID"`
  438. AccountName string `json:"azureStorageAccount"`
  439. AccessKey string `json:"azureStorageAccessKey"`
  440. ContainerName string `json:"azureStorageContainer"`
  441. ContainerPath string `json:"azureContainerPath"`
  442. AzureCloud string `json:"azureCloud"`
  443. }
  444. // IsEmpty returns true if all fields in config are empty, false if not.
  445. func (asc *AzureStorageConfig) IsEmpty() bool {
  446. return asc.SubscriptionId == "" &&
  447. asc.AccountName == "" &&
  448. asc.AccessKey == "" &&
  449. asc.ContainerName == "" &&
  450. asc.ContainerPath == "" &&
  451. asc.AzureCloud == ""
  452. }
  453. // Represents an azure app key
  454. type AzureAppKey struct {
  455. AppID string `json:"appId"`
  456. DisplayName string `json:"displayName"`
  457. Name string `json:"name"`
  458. Password string `json:"password"`
  459. Tenant string `json:"tenant"`
  460. }
  461. // Azure service key for a specific subscription
  462. type AzureServiceKey struct {
  463. SubscriptionID string `json:"subscriptionId"`
  464. ServiceKey *AzureAppKey `json:"serviceKey"`
  465. }
  466. // Validity check on service key
  467. func (ask *AzureServiceKey) IsValid() bool {
  468. return ask.SubscriptionID != "" &&
  469. ask.ServiceKey != nil &&
  470. ask.ServiceKey.AppID != "" &&
  471. ask.ServiceKey.Password != "" &&
  472. ask.ServiceKey.Tenant != ""
  473. }
  474. // Loads the azure authentication via configuration or a secret set at install time.
  475. func (az *Azure) getAzureRateCardAuth(forceReload bool, cp *CustomPricing) (subscriptionID, clientID, clientSecret, tenantID string) {
  476. // 1. Check for secret (secret values will always be used if they are present)
  477. s, _ := az.loadAzureAuthSecret(forceReload)
  478. if s != nil && s.IsValid() {
  479. subscriptionID = s.SubscriptionID
  480. clientID = s.ServiceKey.AppID
  481. clientSecret = s.ServiceKey.Password
  482. tenantID = s.ServiceKey.Tenant
  483. return
  484. }
  485. // 2. Check config values (set though endpoint)
  486. if cp.AzureSubscriptionID != "" && cp.AzureClientID != "" && cp.AzureClientSecret != "" && cp.AzureTenantID != "" {
  487. subscriptionID = cp.AzureSubscriptionID
  488. clientID = cp.AzureClientID
  489. clientSecret = cp.AzureClientSecret
  490. tenantID = cp.AzureTenantID
  491. return
  492. }
  493. // 3. Check if AzureSubscriptionID is set in config (set though endpoint)
  494. // MSI credentials will be attempted if the subscription ID is set, but clientID, clientSecret and tenantID are not
  495. if cp.AzureSubscriptionID != "" {
  496. subscriptionID = cp.AzureSubscriptionID
  497. return
  498. }
  499. // 4. Empty values
  500. return "", "", "", ""
  501. }
  502. // GetAzureStorageConfig retrieves storage config from secret and sets default values
  503. func (az *Azure) GetAzureStorageConfig(forceReload bool, cp *CustomPricing) (*AzureStorageConfig, error) {
  504. // default subscription id
  505. defaultSubscriptionID := cp.AzureSubscriptionID
  506. // 1. Check Config for storage set up
  507. asc := &AzureStorageConfig{
  508. SubscriptionId: cp.AzureStorageSubscriptionID,
  509. AccountName: cp.AzureStorageAccount,
  510. AccessKey: cp.AzureStorageAccessKey,
  511. ContainerName: cp.AzureStorageContainer,
  512. ContainerPath: cp.AzureContainerPath,
  513. AzureCloud: cp.AzureCloud,
  514. }
  515. // check for required fields
  516. if asc != nil && asc.AccessKey != "" && asc.AccountName != "" && asc.ContainerName != "" && asc.SubscriptionId != "" {
  517. az.serviceAccountChecks.set("hasStorage", &ServiceAccountCheck{
  518. Message: "Azure Storage Config exists",
  519. Status: true,
  520. })
  521. return asc, nil
  522. }
  523. // 2. Check for secret
  524. asc, err := az.loadAzureStorageConfig(forceReload)
  525. if err != nil {
  526. log.Errorf("Error, %s", err.Error())
  527. } else if asc != nil {
  528. // To support already configured users, subscriptionID may not be set in secret in which case, the subscriptionID
  529. // for the rate card API is used
  530. if asc.SubscriptionId == "" {
  531. asc.SubscriptionId = defaultSubscriptionID
  532. }
  533. // check for required fields
  534. if asc.AccessKey != "" && asc.AccountName != "" && asc.ContainerName != "" && asc.SubscriptionId != "" {
  535. az.serviceAccountChecks.set("hasStorage", &ServiceAccountCheck{
  536. Message: "Azure Storage Config exists",
  537. Status: true,
  538. })
  539. return asc, nil
  540. }
  541. }
  542. az.serviceAccountChecks.set("hasStorage", &ServiceAccountCheck{
  543. Message: "Azure Storage Config exists",
  544. Status: false,
  545. })
  546. return nil, fmt.Errorf("azure storage config not found")
  547. }
  548. // Load once and cache the result (even on failure). This is an install time secret, so
  549. // we don't expect the secret to change. If it does, however, we can force reload using
  550. // the input parameter.
  551. func (az *Azure) loadAzureAuthSecret(force bool) (*AzureServiceKey, error) {
  552. if !force && az.loadedAzureSecret {
  553. return az.azureSecret, nil
  554. }
  555. az.loadedAzureSecret = true
  556. exists, err := fileutil.FileExists(authSecretPath)
  557. if !exists || err != nil {
  558. return nil, fmt.Errorf("Failed to locate service account file: %s", authSecretPath)
  559. }
  560. result, err := os.ReadFile(authSecretPath)
  561. if err != nil {
  562. return nil, err
  563. }
  564. var ask AzureServiceKey
  565. err = json.Unmarshal(result, &ask)
  566. if err != nil {
  567. return nil, err
  568. }
  569. az.azureSecret = &ask
  570. return &ask, nil
  571. }
  572. // Load once and cache the result (even on failure). This is an install time secret, so
  573. // we don't expect the secret to change. If it does, however, we can force reload using
  574. // the input parameter.
  575. func (az *Azure) loadAzureStorageConfig(force bool) (*AzureStorageConfig, error) {
  576. if !force && az.loadedAzureStorageConfigSecret {
  577. return az.azureStorageConfig, nil
  578. }
  579. az.loadedAzureStorageConfigSecret = true
  580. exists, err := fileutil.FileExists(storageConfigSecretPath)
  581. if !exists || err != nil {
  582. return nil, fmt.Errorf("Failed to locate azure storage config file: %s", storageConfigSecretPath)
  583. }
  584. result, err := os.ReadFile(storageConfigSecretPath)
  585. if err != nil {
  586. return nil, err
  587. }
  588. var asc AzureStorageConfig
  589. err = json.Unmarshal(result, &asc)
  590. if err != nil {
  591. return nil, err
  592. }
  593. az.azureStorageConfig = &asc
  594. return &asc, nil
  595. }
  596. func (az *Azure) GetKey(labels map[string]string, n *v1.Node) Key {
  597. cfg, err := az.GetConfig()
  598. if err != nil {
  599. log.Infof("Error loading azure custom pricing information")
  600. }
  601. // azure defaults, see https://docs.microsoft.com/en-us/azure/aks/gpu-cluster
  602. gpuLabel := "accelerator"
  603. gpuLabelValue := "nvidia"
  604. if cfg.GpuLabel != "" {
  605. gpuLabel = cfg.GpuLabel
  606. }
  607. if cfg.GpuLabelValue != "" {
  608. gpuLabelValue = cfg.GpuLabelValue
  609. }
  610. return &azureKey{
  611. Labels: labels,
  612. GPULabel: gpuLabel,
  613. GPULabelValue: gpuLabelValue,
  614. }
  615. }
  616. // CreateString builds strings effectively
  617. func createString(keys ...string) string {
  618. var b strings.Builder
  619. for _, key := range keys {
  620. b.WriteString(key)
  621. }
  622. return b.String()
  623. }
  624. func transformMachineType(subCategory string, mt []string) []string {
  625. switch {
  626. case strings.Contains(subCategory, "Basic"):
  627. return []string{createString("Basic_", mt[0])}
  628. case len(mt) == 2:
  629. return []string{createString("Standard_", mt[0]), createString("Standard_", mt[1])}
  630. default:
  631. return []string{createString("Standard_", mt[0])}
  632. }
  633. }
  634. func addSuffix(mt string, suffixes ...string) []string {
  635. result := make([]string, len(suffixes))
  636. var suffix string
  637. parts := strings.Split(mt, "_")
  638. if len(parts) > 2 {
  639. for _, p := range parts[2:] {
  640. suffix = createString(suffix, "_", p)
  641. }
  642. }
  643. for i, s := range suffixes {
  644. result[i] = createString(parts[0], "_", parts[1], s, suffix)
  645. }
  646. return result
  647. }
  648. func getMachineTypeVariants(mt string) []string {
  649. switch {
  650. case mtStandardB.MatchString(mt):
  651. return []string{createString(mt, "s")}
  652. case mtStandardD.MatchString(mt):
  653. var result []string
  654. result = append(result, addSuffix(mt, "s")[0])
  655. dsType := strings.Replace(mt, "Standard_D", "Standard_DS", -1)
  656. result = append(result, dsType)
  657. result = append(result, addSuffix(dsType, "-1", "-2", "-4", "-8")...)
  658. return result
  659. case mtStandardE.MatchString(mt):
  660. return addSuffix(mt, "s", "-2s", "-4s", "-8s", "-16s", "-32s")
  661. case mtStandardF.MatchString(mt):
  662. return addSuffix(mt, "s")
  663. case mtStandardG.MatchString(mt):
  664. var result []string
  665. gsType := strings.Replace(mt, "Standard_G", "Standard_GS", -1)
  666. result = append(result, gsType)
  667. return append(result, addSuffix(gsType, "-4", "-8", "-16")...)
  668. case mtStandardL.MatchString(mt):
  669. return addSuffix(mt, "s")
  670. case mtStandardM.MatchString(mt) && strings.HasSuffix(mt, "ms"):
  671. base := strings.TrimSuffix(mt, "ms")
  672. return addSuffix(base, "-2ms", "-4ms", "-8ms", "-16ms", "-32ms", "-64ms")
  673. case mtStandardM.MatchString(mt) && (strings.HasSuffix(mt, "ls") || strings.HasSuffix(mt, "ts")):
  674. return []string{}
  675. case mtStandardM.MatchString(mt) && strings.HasSuffix(mt, "s"):
  676. base := strings.TrimSuffix(mt, "s")
  677. return addSuffix(base, "", "m")
  678. case mtStandardN.MatchString(mt):
  679. return addSuffix(mt, "s")
  680. }
  681. return []string{}
  682. }
  683. func (az *Azure) GetManagementPlatform() (string, error) {
  684. nodes := az.Clientset.GetAllNodes()
  685. if len(nodes) > 0 {
  686. n := nodes[0]
  687. providerID := n.Spec.ProviderID
  688. if strings.Contains(providerID, "aks") {
  689. return "aks", nil
  690. }
  691. }
  692. return "", nil
  693. }
  694. // DownloadPricingData uses provided azure "best guesses" for pricing
  695. func (az *Azure) DownloadPricingData() error {
  696. az.DownloadPricingDataLock.Lock()
  697. defer az.DownloadPricingDataLock.Unlock()
  698. config, err := az.GetConfig()
  699. if err != nil {
  700. az.RateCardPricingError = err
  701. return err
  702. }
  703. // Load the service provider keys
  704. subscriptionID, clientID, clientSecret, tenantID := az.getAzureRateCardAuth(false, config)
  705. config.AzureSubscriptionID = subscriptionID
  706. config.AzureClientID = clientID
  707. config.AzureClientSecret = clientSecret
  708. config.AzureTenantID = tenantID
  709. var authorizer autorest.Authorizer
  710. azureEnv := determineCloudByRegion(az.clusterRegion)
  711. if config.AzureClientID != "" && config.AzureClientSecret != "" && config.AzureTenantID != "" {
  712. credentialsConfig := NewClientCredentialsConfig(config.AzureClientID, config.AzureClientSecret, config.AzureTenantID, azureEnv)
  713. a, err := credentialsConfig.Authorizer()
  714. if err != nil {
  715. az.RateCardPricingError = err
  716. return err
  717. }
  718. authorizer = a
  719. }
  720. if authorizer == nil {
  721. a, err := auth.NewAuthorizerFromEnvironment()
  722. authorizer = a
  723. if err != nil {
  724. a, err := auth.NewAuthorizerFromFile(azureEnv.ResourceManagerEndpoint)
  725. if err != nil {
  726. az.RateCardPricingError = err
  727. return err
  728. }
  729. authorizer = a
  730. }
  731. }
  732. sClient := subscriptions.NewClientWithBaseURI(azureEnv.ResourceManagerEndpoint)
  733. sClient.Authorizer = authorizer
  734. rcClient := commerce.NewRateCardClientWithBaseURI(azureEnv.ResourceManagerEndpoint, config.AzureSubscriptionID)
  735. rcClient.Authorizer = authorizer
  736. providersClient := resources.NewProvidersClientWithBaseURI(azureEnv.ResourceManagerEndpoint, config.AzureSubscriptionID)
  737. providersClient.Authorizer = authorizer
  738. rateCardFilter := fmt.Sprintf("OfferDurableId eq '%s' and Currency eq '%s' and Locale eq 'en-US' and RegionInfo eq '%s'", config.AzureOfferDurableID, config.CurrencyCode, config.AzureBillingRegion)
  739. log.Infof("Using ratecard query %s", rateCardFilter)
  740. result, err := rcClient.Get(context.TODO(), rateCardFilter)
  741. if err != nil {
  742. log.Warnf("Error in pricing download query from API")
  743. az.RateCardPricingError = err
  744. return err
  745. }
  746. regions, err := getRegions("compute", sClient, providersClient, config.AzureSubscriptionID)
  747. if err != nil {
  748. log.Warnf("Error in pricing download regions from API")
  749. az.RateCardPricingError = err
  750. return err
  751. }
  752. baseCPUPrice := config.CPU
  753. allPrices := make(map[string]*AzurePricing)
  754. for _, v := range *result.Meters {
  755. meterName := *v.MeterName
  756. meterRegion := *v.MeterRegion
  757. meterCategory := *v.MeterCategory
  758. meterSubCategory := *v.MeterSubCategory
  759. region, err := toRegionID(meterRegion, regions)
  760. if err != nil {
  761. continue
  762. }
  763. if !strings.Contains(meterSubCategory, "Windows") {
  764. if strings.Contains(meterCategory, "Storage") {
  765. if strings.Contains(meterSubCategory, "HDD") || strings.Contains(meterSubCategory, "SSD") || strings.Contains(meterSubCategory, "Premium Files") {
  766. var storageClass string = ""
  767. if strings.Contains(meterName, "P4 ") {
  768. storageClass = AzureDiskPremiumSSDStorageClass
  769. } else if strings.Contains(meterName, "E4 ") {
  770. storageClass = AzureDiskStandardSSDStorageClass
  771. } else if strings.Contains(meterName, "S4 ") {
  772. storageClass = AzureDiskStandardStorageClass
  773. } else if strings.Contains(meterName, "LRS Provisioned") {
  774. storageClass = AzureFilePremiumStorageClass
  775. }
  776. if storageClass != "" {
  777. var priceInUsd float64
  778. if len(v.MeterRates) < 1 {
  779. log.Warnf("missing rate info %+v", map[string]interface{}{"MeterSubCategory": *v.MeterSubCategory, "region": region})
  780. continue
  781. }
  782. for _, rate := range v.MeterRates {
  783. priceInUsd += *rate
  784. }
  785. // rate is in disk per month, resolve price per hour, then GB per hour
  786. pricePerHour := priceInUsd / 730.0 / 32.0
  787. priceStr := fmt.Sprintf("%f", pricePerHour)
  788. key := region + "," + storageClass
  789. log.Debugf("Adding PV.Key: %s, Cost: %s", key, priceStr)
  790. allPrices[key] = &AzurePricing{
  791. PV: &PV{
  792. Cost: priceStr,
  793. Region: region,
  794. },
  795. }
  796. }
  797. }
  798. }
  799. if strings.Contains(meterCategory, "Virtual Machines") {
  800. usageType := ""
  801. if !strings.Contains(meterName, "Low Priority") {
  802. usageType = "ondemand"
  803. } else {
  804. usageType = "preemptible"
  805. }
  806. var instanceTypes []string
  807. name := strings.TrimSuffix(meterName, " Low Priority")
  808. instanceType := strings.Split(name, "/")
  809. for _, it := range instanceType {
  810. if strings.Contains(meterSubCategory, "Promo") {
  811. it = it + " Promo"
  812. }
  813. instanceTypes = append(instanceTypes, strings.Replace(it, " ", "_", 1))
  814. }
  815. instanceTypes = transformMachineType(meterSubCategory, instanceTypes)
  816. if strings.Contains(name, "Expired") {
  817. instanceTypes = []string{}
  818. }
  819. var priceInUsd float64
  820. if len(v.MeterRates) < 1 {
  821. log.Warnf("missing rate info %+v", map[string]interface{}{"MeterSubCategory": *v.MeterSubCategory, "region": region})
  822. continue
  823. }
  824. for _, rate := range v.MeterRates {
  825. priceInUsd += *rate
  826. }
  827. priceStr := fmt.Sprintf("%f", priceInUsd)
  828. for _, instanceType := range instanceTypes {
  829. key := fmt.Sprintf("%s,%s,%s", region, instanceType, usageType)
  830. allPrices[key] = &AzurePricing{
  831. Node: &Node{
  832. Cost: priceStr,
  833. BaseCPUPrice: baseCPUPrice,
  834. UsageType: usageType,
  835. },
  836. }
  837. }
  838. }
  839. }
  840. }
  841. // There is no easy way of supporting Standard Azure-File, because it's billed per used GB
  842. // this will set the price to "0" as a workaround to not spam with `Persistent Volume pricing not found for` error
  843. // check https://github.com/opencost/opencost/issues/159 for more information (same problem on AWS)
  844. zeroPrice := "0.0"
  845. for region := range regions {
  846. key := region + "," + AzureFileStandardStorageClass
  847. log.Debugf("Adding PV.Key: %s, Cost: %s", key, zeroPrice)
  848. allPrices[key] = &AzurePricing{
  849. PV: &PV{
  850. Cost: zeroPrice,
  851. Region: region,
  852. },
  853. }
  854. }
  855. az.Pricing = allPrices
  856. az.RateCardPricingError = nil
  857. return nil
  858. }
  859. // determineCloudByRegion uses region name to pick the correct Cloud Environment for the azure provider to use
  860. func determineCloudByRegion(region string) azure.Environment {
  861. lcRegion := strings.ToLower(region)
  862. if strings.Contains(lcRegion, "china") {
  863. return azure.ChinaCloud
  864. }
  865. if strings.Contains(lcRegion, "gov") || strings.Contains(lcRegion, "dod") {
  866. return azure.USGovernmentCloud
  867. }
  868. // Default to public cloud
  869. return azure.PublicCloud
  870. }
  871. // NewClientCredentialsConfig creates an AuthorizerConfig object configured to obtain an Authorizer through Client Credentials.
  872. func NewClientCredentialsConfig(clientID string, clientSecret string, tenantID string, env azure.Environment) auth.ClientCredentialsConfig {
  873. return auth.ClientCredentialsConfig{
  874. ClientID: clientID,
  875. ClientSecret: clientSecret,
  876. TenantID: tenantID,
  877. Resource: env.ResourceManagerEndpoint,
  878. AADEndpoint: env.ActiveDirectoryEndpoint,
  879. }
  880. }
  881. func (az *Azure) addPricing(features string, azurePricing *AzurePricing) {
  882. if az.Pricing == nil {
  883. az.Pricing = map[string]*AzurePricing{}
  884. }
  885. az.Pricing[features] = azurePricing
  886. }
  887. // AllNodePricing returns the Azure pricing objects stored
  888. func (az *Azure) AllNodePricing() (interface{}, error) {
  889. az.DownloadPricingDataLock.RLock()
  890. defer az.DownloadPricingDataLock.RUnlock()
  891. return az.Pricing, nil
  892. }
  893. // NodePricing returns Azure pricing data for a single node
  894. func (az *Azure) NodePricing(key Key) (*Node, error) {
  895. az.DownloadPricingDataLock.RLock()
  896. defer az.DownloadPricingDataLock.RUnlock()
  897. azKey, ok := key.(*azureKey)
  898. if !ok {
  899. return nil, fmt.Errorf("azure: NodePricing: key is of type %T", key)
  900. }
  901. config, _ := az.GetConfig()
  902. if slv, ok := azKey.Labels[config.SpotLabel]; ok && slv == config.SpotLabelValue && config.SpotLabel != "" && config.SpotLabelValue != "" {
  903. features := strings.Split(azKey.Features(), ",")
  904. region := features[0]
  905. instance := features[1]
  906. spotFeatures := fmt.Sprintf("%s,%s,%s", region, instance, "spot")
  907. if n, ok := az.Pricing[spotFeatures]; ok {
  908. log.DedupedInfof(5, "Returning pricing for node %s: %+v from key %s", azKey, n, spotFeatures)
  909. if azKey.isValidGPUNode() {
  910. n.Node.GPU = "1" // TODO: support multiple GPUs
  911. }
  912. return n.Node, nil
  913. }
  914. log.Infof("[Info] found spot instance, trying to get retail price for %s: %s, ", spotFeatures, azKey)
  915. spotCost, err := getRetailPrice(region, instance, config.CurrencyCode, true)
  916. if err != nil {
  917. log.DedupedWarningf(5, "failed to retrieve spot retail pricing")
  918. } else {
  919. gpu := ""
  920. if azKey.isValidGPUNode() {
  921. gpu = "1"
  922. }
  923. spotNode := &Node{
  924. Cost: spotCost,
  925. UsageType: "spot",
  926. GPU: gpu,
  927. }
  928. az.addPricing(spotFeatures, &AzurePricing{
  929. Node: spotNode,
  930. })
  931. return spotNode, nil
  932. }
  933. }
  934. if n, ok := az.Pricing[azKey.Features()]; ok {
  935. log.Debugf("Returning pricing for node %s: %+v from key %s", azKey, n, azKey.Features())
  936. if azKey.isValidGPUNode() {
  937. n.Node.GPU = azKey.GetGPUCount()
  938. }
  939. return n.Node, nil
  940. }
  941. log.Warnf("no pricing data found for %s: %s", azKey.Features(), azKey)
  942. c, err := az.GetConfig()
  943. if err != nil {
  944. return nil, fmt.Errorf("No default pricing data available")
  945. }
  946. if azKey.isValidGPUNode() {
  947. return &Node{
  948. VCPUCost: c.CPU,
  949. RAMCost: c.RAM,
  950. UsesBaseCPUPrice: true,
  951. GPUCost: c.GPU,
  952. GPU: azKey.GetGPUCount(),
  953. }, nil
  954. }
  955. return &Node{
  956. VCPUCost: c.CPU,
  957. RAMCost: c.RAM,
  958. UsesBaseCPUPrice: true,
  959. }, nil
  960. }
  961. // Stubbed NetworkPricing for Azure. Pull directly from azure.json for now
  962. func (az *Azure) NetworkPricing() (*Network, error) {
  963. cpricing, err := az.Config.GetCustomPricingData()
  964. if err != nil {
  965. return nil, err
  966. }
  967. znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
  968. if err != nil {
  969. return nil, err
  970. }
  971. rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
  972. if err != nil {
  973. return nil, err
  974. }
  975. inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
  976. if err != nil {
  977. return nil, err
  978. }
  979. return &Network{
  980. ZoneNetworkEgressCost: znec,
  981. RegionNetworkEgressCost: rnec,
  982. InternetNetworkEgressCost: inec,
  983. }, nil
  984. }
  985. // LoadBalancerPricing on Azure, LoadBalancer services correspond to public IPs. For now the pricing of LoadBalancer
  986. // services will be that of a standard static public IP https://azure.microsoft.com/en-us/pricing/details/ip-addresses/.
  987. // Azure still has load balancers which follow the standard pricing scheme based on rules
  988. // https://azure.microsoft.com/en-us/pricing/details/load-balancer/, they are created on a per-cluster basis.
  989. func (azr *Azure) LoadBalancerPricing() (*LoadBalancer, error) {
  990. return &LoadBalancer{
  991. Cost: 0.005,
  992. }, nil
  993. }
  994. type azurePvKey struct {
  995. Labels map[string]string
  996. StorageClass string
  997. StorageClassParameters map[string]string
  998. DefaultRegion string
  999. ProviderId string
  1000. }
  1001. func (az *Azure) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
  1002. providerID := ""
  1003. if pv.Spec.AzureDisk != nil {
  1004. providerID = pv.Spec.AzureDisk.DiskName
  1005. }
  1006. return &azurePvKey{
  1007. Labels: pv.Labels,
  1008. StorageClass: pv.Spec.StorageClassName,
  1009. StorageClassParameters: parameters,
  1010. DefaultRegion: defaultRegion,
  1011. ProviderId: providerID,
  1012. }
  1013. }
  1014. func (key *azurePvKey) ID() string {
  1015. return key.ProviderId
  1016. }
  1017. func (key *azurePvKey) GetStorageClass() string {
  1018. return key.StorageClass
  1019. }
  1020. func (key *azurePvKey) Features() string {
  1021. storageClass := key.StorageClassParameters["storageaccounttype"]
  1022. storageSKU := key.StorageClassParameters["skuName"]
  1023. if storageClass != "" {
  1024. if strings.EqualFold(storageClass, "Premium_LRS") {
  1025. storageClass = AzureDiskPremiumSSDStorageClass
  1026. } else if strings.EqualFold(storageClass, "StandardSSD_LRS") {
  1027. storageClass = AzureDiskStandardSSDStorageClass
  1028. } else if strings.EqualFold(storageClass, "Standard_LRS") {
  1029. storageClass = AzureDiskStandardStorageClass
  1030. }
  1031. } else {
  1032. if strings.EqualFold(storageSKU, "Premium_LRS") {
  1033. storageClass = AzureFilePremiumStorageClass
  1034. } else if strings.EqualFold(storageSKU, "Standard_LRS") {
  1035. storageClass = AzureFileStandardStorageClass
  1036. }
  1037. }
  1038. if region, ok := util.GetRegion(key.Labels); ok {
  1039. return region + "," + storageClass
  1040. }
  1041. return key.DefaultRegion + "," + storageClass
  1042. }
  1043. func (*Azure) GetAddresses() ([]byte, error) {
  1044. return nil, nil
  1045. }
  1046. func (az *Azure) GetDisks() ([]byte, error) {
  1047. disks, err := az.getDisks()
  1048. if err != nil {
  1049. return nil, err
  1050. }
  1051. return json.Marshal(disks)
  1052. }
  1053. func (az *Azure) getDisks() ([]*compute.Disk, error) {
  1054. config, err := az.GetConfig()
  1055. if err != nil {
  1056. return nil, err
  1057. }
  1058. // Load the service provider keys
  1059. subscriptionID, clientID, clientSecret, tenantID := az.getAzureRateCardAuth(false, config)
  1060. config.AzureSubscriptionID = subscriptionID
  1061. config.AzureClientID = clientID
  1062. config.AzureClientSecret = clientSecret
  1063. config.AzureTenantID = tenantID
  1064. var authorizer autorest.Authorizer
  1065. azureEnv := determineCloudByRegion(az.clusterRegion)
  1066. if config.AzureClientID != "" && config.AzureClientSecret != "" && config.AzureTenantID != "" {
  1067. credentialsConfig := NewClientCredentialsConfig(config.AzureClientID, config.AzureClientSecret, config.AzureTenantID, azureEnv)
  1068. a, err := credentialsConfig.Authorizer()
  1069. if err != nil {
  1070. az.RateCardPricingError = err
  1071. return nil, err
  1072. }
  1073. authorizer = a
  1074. }
  1075. if authorizer == nil {
  1076. a, err := auth.NewAuthorizerFromEnvironment()
  1077. authorizer = a
  1078. if err != nil {
  1079. a, err := auth.NewAuthorizerFromFile(azureEnv.ResourceManagerEndpoint)
  1080. if err != nil {
  1081. az.RateCardPricingError = err
  1082. return nil, err
  1083. }
  1084. authorizer = a
  1085. }
  1086. }
  1087. client := compute.NewDisksClient(config.AzureSubscriptionID)
  1088. client.Authorizer = authorizer
  1089. ctx := context.TODO()
  1090. var disks []*compute.Disk
  1091. diskPage, err := client.List(ctx)
  1092. if err != nil {
  1093. return nil, fmt.Errorf("error getting disks: %v", err)
  1094. }
  1095. for diskPage.NotDone() {
  1096. for _, d := range diskPage.Values() {
  1097. d := d
  1098. disks = append(disks, &d)
  1099. }
  1100. err := diskPage.NextWithContext(context.Background())
  1101. if err != nil {
  1102. return nil, fmt.Errorf("error getting next page: %v", err)
  1103. }
  1104. }
  1105. return disks, nil
  1106. }
  1107. func (az *Azure) isDiskOrphaned(disk *compute.Disk) bool {
  1108. //TODO: needs better algorithm
  1109. return disk.DiskState == "Unattached" || disk.DiskState == "Reserved"
  1110. }
  1111. func (az *Azure) GetOrphanedResources() ([]OrphanedResource, error) {
  1112. disks, err := az.getDisks()
  1113. if err != nil {
  1114. return nil, err
  1115. }
  1116. var orphanedResources []OrphanedResource
  1117. for _, d := range disks {
  1118. if az.isDiskOrphaned(d) {
  1119. cost, err := az.findCostForDisk(d)
  1120. if err != nil {
  1121. return nil, err
  1122. }
  1123. diskName := ""
  1124. if d.Name != nil {
  1125. diskName = *d.Name
  1126. }
  1127. diskRegion := ""
  1128. if d.Location != nil {
  1129. diskRegion = *d.Location
  1130. }
  1131. var diskSize int64
  1132. if d.DiskSizeGB != nil {
  1133. diskSize = int64(*d.DiskSizeGB)
  1134. }
  1135. desc := map[string]string{}
  1136. for k, v := range d.Tags {
  1137. if v == nil {
  1138. desc[k] = ""
  1139. } else {
  1140. desc[k] = *v
  1141. }
  1142. }
  1143. or := OrphanedResource{
  1144. Kind: "disk",
  1145. Region: diskRegion,
  1146. Description: desc,
  1147. Size: &diskSize,
  1148. DiskName: diskName,
  1149. MonthlyCost: &cost,
  1150. }
  1151. orphanedResources = append(orphanedResources, or)
  1152. }
  1153. }
  1154. return orphanedResources, nil
  1155. }
  1156. func (az *Azure) findCostForDisk(d *compute.Disk) (float64, error) {
  1157. if d == nil {
  1158. return 0.0, fmt.Errorf("disk is empty")
  1159. }
  1160. storageClass := string(d.Sku.Name)
  1161. if strings.EqualFold(storageClass, "Premium_LRS") {
  1162. storageClass = AzureDiskPremiumSSDStorageClass
  1163. } else if strings.EqualFold(storageClass, "StandardSSD_LRS") {
  1164. storageClass = AzureDiskStandardSSDStorageClass
  1165. } else if strings.EqualFold(storageClass, "Standard_LRS") {
  1166. storageClass = AzureDiskStandardStorageClass
  1167. }
  1168. key := *d.Location + "," + storageClass
  1169. diskPricePerGBHour, err := strconv.ParseFloat(az.Pricing[key].PV.Cost, 64)
  1170. if err != nil {
  1171. return 0.0, fmt.Errorf("error converting to float: %s", err)
  1172. }
  1173. cost := diskPricePerGBHour * timeutil.HoursPerMonth * float64(*d.DiskSizeGB)
  1174. return cost, nil
  1175. }
  1176. func (az *Azure) ClusterInfo() (map[string]string, error) {
  1177. remoteEnabled := env.IsRemoteEnabled()
  1178. m := make(map[string]string)
  1179. m["name"] = "Azure Cluster #1"
  1180. c, err := az.GetConfig()
  1181. if err != nil {
  1182. return nil, err
  1183. }
  1184. if c.ClusterName != "" {
  1185. m["name"] = c.ClusterName
  1186. }
  1187. m["provider"] = kubecost.AzureProvider
  1188. m["account"] = az.clusterAccountId
  1189. m["region"] = az.clusterRegion
  1190. m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
  1191. m["id"] = env.GetClusterID()
  1192. return m, nil
  1193. }
  1194. func (az *Azure) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
  1195. return az.Config.UpdateFromMap(a)
  1196. }
  1197. func (az *Azure) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  1198. return az.Config.Update(func(c *CustomPricing) error {
  1199. if updateType == AzureStorageUpdateType {
  1200. asc := &AzureStorageConfig{}
  1201. err := json.NewDecoder(r).Decode(&asc)
  1202. if err != nil {
  1203. return fmt.Errorf("error decoding AzureStorageConfig: %s", err)
  1204. }
  1205. c.AzureStorageSubscriptionID = asc.SubscriptionId
  1206. c.AzureStorageAccount = asc.AccountName
  1207. if asc.AccessKey != "" {
  1208. c.AzureStorageAccessKey = asc.AccessKey
  1209. }
  1210. c.AzureStorageContainer = asc.ContainerName
  1211. c.AzureContainerPath = asc.ContainerPath
  1212. c.AzureCloud = asc.AzureCloud
  1213. } else {
  1214. // This will block if not in a goroutine. It calls GetConfig(), which
  1215. // in turn calls GetCustomPricingData, which acquires the same lock
  1216. // that is acquired by az.Config.Update, which is the function to
  1217. // which this function gets passed, and subsequently called. Booo.
  1218. defer func() {
  1219. go az.DownloadPricingData()
  1220. }()
  1221. a := make(map[string]interface{})
  1222. err := json.NewDecoder(r).Decode(&a)
  1223. if err != nil {
  1224. return fmt.Errorf("error decoding AzureStorageConfig: %s", err)
  1225. }
  1226. for k, v := range a {
  1227. // Just so we consistently supply / receive the same values, uppercase the first letter.
  1228. kUpper := toTitle.String(k)
  1229. vstr, ok := v.(string)
  1230. if ok {
  1231. err := SetCustomPricingField(c, kUpper, vstr)
  1232. if err != nil {
  1233. return fmt.Errorf("error setting custom pricing field on AzureStorageConfig: %s", err)
  1234. }
  1235. } else {
  1236. return fmt.Errorf("type error while updating config for %s", kUpper)
  1237. }
  1238. }
  1239. }
  1240. if env.IsRemoteEnabled() {
  1241. err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
  1242. if err != nil {
  1243. return fmt.Errorf("error updating cluster metadata: %s", err)
  1244. }
  1245. }
  1246. return nil
  1247. })
  1248. }
  1249. func (az *Azure) GetConfig() (*CustomPricing, error) {
  1250. c, err := az.Config.GetCustomPricingData()
  1251. if err != nil {
  1252. return nil, err
  1253. }
  1254. if c.Discount == "" {
  1255. c.Discount = "0%"
  1256. }
  1257. if c.NegotiatedDiscount == "" {
  1258. c.NegotiatedDiscount = "0%"
  1259. }
  1260. if c.CurrencyCode == "" {
  1261. c.CurrencyCode = "USD"
  1262. }
  1263. if c.AzureBillingRegion == "" {
  1264. c.AzureBillingRegion = "US"
  1265. }
  1266. // Default to pay-as-you-go Durable offer id
  1267. if c.AzureOfferDurableID == "" {
  1268. c.AzureOfferDurableID = "MS-AZR-0003p"
  1269. }
  1270. if c.ShareTenancyCosts == "" {
  1271. c.ShareTenancyCosts = defaultShareTenancyCost
  1272. }
  1273. if c.SpotLabel == "" {
  1274. c.SpotLabel = defaultSpotLabel
  1275. }
  1276. if c.SpotLabelValue == "" {
  1277. c.SpotLabelValue = defaultSpotLabelValue
  1278. }
  1279. return c, nil
  1280. }
  1281. func (az *Azure) ApplyReservedInstancePricing(nodes map[string]*Node) {
  1282. }
  1283. func (az *Azure) PVPricing(pvk PVKey) (*PV, error) {
  1284. az.DownloadPricingDataLock.RLock()
  1285. defer az.DownloadPricingDataLock.RUnlock()
  1286. pricing, ok := az.Pricing[pvk.Features()]
  1287. if !ok {
  1288. log.Debugf("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
  1289. return &PV{}, nil
  1290. }
  1291. return pricing.PV, nil
  1292. }
  1293. func (az *Azure) GetLocalStorageQuery(window, offset time.Duration, rate bool, used bool) string {
  1294. return ""
  1295. }
  1296. func (az *Azure) ServiceAccountStatus() *ServiceAccountStatus {
  1297. return az.serviceAccountChecks.getStatus()
  1298. }
  1299. const rateCardPricingSource = "Rate Card API"
  1300. // PricingSourceStatus returns the status of the rate card api
  1301. func (az *Azure) PricingSourceStatus() map[string]*PricingSource {
  1302. sources := make(map[string]*PricingSource)
  1303. errMsg := ""
  1304. if az.RateCardPricingError != nil {
  1305. errMsg = az.RateCardPricingError.Error()
  1306. }
  1307. rcps := &PricingSource{
  1308. Name: rateCardPricingSource,
  1309. Enabled: true,
  1310. Error: errMsg,
  1311. }
  1312. if rcps.Error != "" {
  1313. rcps.Available = false
  1314. } else if len(az.Pricing) == 0 {
  1315. rcps.Error = "No Pricing Data Available"
  1316. rcps.Available = false
  1317. } else {
  1318. rcps.Available = true
  1319. }
  1320. sources[rateCardPricingSource] = rcps
  1321. return sources
  1322. }
  1323. func (*Azure) ClusterManagementPricing() (string, float64, error) {
  1324. return "", 0.0, nil
  1325. }
  1326. func (az *Azure) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
  1327. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  1328. }
  1329. func (az *Azure) Regions() []string {
  1330. regionOverrides := env.GetRegionOverrideList()
  1331. if len(regionOverrides) > 0 {
  1332. log.Debugf("Overriding Azure regions with configured region list: %+v", regionOverrides)
  1333. return regionOverrides
  1334. }
  1335. return azureRegions
  1336. }
  1337. func parseAzureSubscriptionID(id string) string {
  1338. match := azureSubRegex.FindStringSubmatch(id)
  1339. if len(match) >= 2 {
  1340. return match[1]
  1341. }
  1342. // Return empty string if an account could not be parsed from provided string
  1343. return ""
  1344. }