azureprovider.go 41 KB

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