Просмотр исходного кода

Merge pull request #107 from mda590/mda590-aws-pv-pricing

Add AWS PV Pricing
Ajay Tripathy 7 лет назад
Родитель
Сommit
dbc4481968
2 измененных файлов с 155 добавлено и 32 удалено
  1. 154 32
      cloud/awsprovider.go
  2. 1 0
      cloud/provider.go

+ 154 - 32
cloud/awsprovider.go

@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"math"
 	"net/http"
 	"net/url"
 	"os"
@@ -120,6 +121,7 @@ type AWSProductTerms struct {
 	Storage  string        `json:"storage"`
 	VCpu     string        `json:"vcpu"`
 	GPU      string        `json:"gpu"` // GPU represents the number of GPU on the instance
+	PV       *PV           `json:"pv"`
 }
 
 // ClusterIdEnvVar is the environment variable in which one can manually set the ClusterId
@@ -134,32 +136,76 @@ const ReservedRateCode = ".38NPMPTW36"
 // HourlyRateCode is appended to a node sku
 const HourlyRateCode = ".6YS6EN2CT7"
 
+// volTypes are used to map between AWS UsageTypes and 
+// EBS volume types, as they would appear in K8s storage class
+// name and the EC2 API.
+var volTypes = map[string]string{
+	"EBS:VolumeUsage.gp2":     "gp2",
+	"EBS:VolumeUsage":         "standard",
+	"EBS:VolumeUsage.sc1":     "sc1",
+	"EBS:VolumeUsage.st1":     "st1",
+	"EBS:VolumeUsage.piops":   "io1",
+	"gp2":                     "EBS:VolumeUsage.gp2",
+	"standard":                "EBS:VolumeUsage",
+	"sc1":                     "EBS:VolumeUsage.sc1",
+	"io1":                     "EBS:VolumeUsage.piops",
+	"st1":                     "EBS:VolumeUsage.st1",
+}
+
+// locationToRegion maps AWS region names (As they come from Billing)
+// to actual region identifiers
+var locationToRegion = map[string]string{
+	"US East (Ohio)":             "us-east-2",
+	"US East (N. Virginia)":      "us-east-1",
+	"US West (N. California)":    "us-west-1",
+	"US West (Oregon)":           "us-west-2",
+	"Asia Pacific (Hong Kong)":   "ap-east-1",
+	"Asia Pacific (Mumbai)":      "ap-south-1",
+	"Asia Pacific (Osaka-Local)": "ap-northeast-3",
+	"Asia Pacific (Seoul)":       "ap-northeast-2",
+	"Asia Pacific (Singapore)":   "ap-southeast-1",
+	"Asia Pacific (Sydney)":      "ap-southeast-2",
+	"Asia Pacific (Tokyo)":       "ap-northeast-1",
+	"Canada (Central)":           "ca-central-1",
+	"China (Beijing)":            "cn-north-1",
+	"China (Ningxia)":            "cn-northwest-1",
+	"EU (Frankfurt)":             "eu-central-1",
+	"EU (Ireland)":               "eu-west-1",
+	"EU (London)":                "eu-west-2",
+	"EU (Paris)":                 "eu-west-3",
+	"EU (Stockholm)":             "eu-north-1",
+	"South America (Sao Paulo)":  "sa-east-1",
+	"AWS GovCloud (US-East)":     "us-gov-east-1",
+	"AWS GovCloud (US)":          "us-gov-west-1",
+}
+
+var regionToBillingRegionCode = map[string]string{
+	"us-east-2":			"USE2",
+	"us-east-1":			"",
+	"us-west-1":			"USW1",
+	"us-west-2":			"USW2",
+	"ap-east-1":			"APE1",
+	"ap-south-1":			"APS3",
+	"ap-northeast-3":		"APN3",
+	"ap-northeast-2":		"APN2",
+	"ap-southeast-1":		"APS1",
+	"ap-southeast-2":		"APS2",
+	"ap-northeast-1":		"APN1",
+	"ca-central-1":			"CAN1",
+	"cn-north-1":			"",
+	"cn-northwest-1":		"",
+	"eu-central-1":			"EUC1",
+	"eu-west-1":			"EU",
+	"eu-west-2":			"EUW2",
+	"eu-west-3":			"EUW3",
+	"eu-north-1":			"EUN1",
+	"sa-east-1":			"SAE1",
+	"us-gov-east-1":		"UGE1",
+	"us-gov-west-1":		"UGW1",
+}
+
 // KubeAttrConversion maps the k8s labels for region to an aws region
 func (aws *AWS) KubeAttrConversion(location, instanceType, operatingSystem string) string {
-	locationToRegion := map[string]string{
-		"US East (Ohio)":             "us-east-2",
-		"US East (N. Virginia)":      "us-east-1",
-		"US West (N. California)":    "us-west-1",
-		"US West (Oregon)":           "us-west-2",
-		"Asia Pacific (Mumbai)":      "ap-south-1",
-		"Asia Pacific (Osaka-Local)": "ap-northeast-3",
-		"Asia Pacific (Seoul)":       "ap-northeast-2",
-		"Asia Pacific (Singapore)":   "ap-southeast-1",
-		"Asia Pacific (Sydney)":      "ap-southeast-2",
-		"Asia Pacific (Tokyo)":       "ap-northeast-1",
-		"Canada (Central)":           "ca-central-1",
-		"China (Beijing)":            "cn-north-1",
-		"China (Ningxia)":            "cn-northwest-1",
-		"EU (Frankfurt)":             "eu-central-1",
-		"EU (Ireland)":               "eu-west-1",
-		"EU (London)":                "eu-west-2",
-		"EU (Paris)":                 "eu-west-3",
-		"EU (Stockholm)":             "eu-north-1",
-		"South America (São Paulo)":  "sa-east-1",
-		"AWS GovCloud (US-East)":     "us-gov-east-1",
-		"AWS GovCloud (US)":          "us-gov-west-1",
-	}
-
 	operatingSystem = strings.ToLower(operatingSystem)
 
 	region := locationToRegion[location]
@@ -307,15 +353,12 @@ func (k *awsKey) Features() string {
 }
 
 func (aws *AWS) PVPricing(pvk PVKey) (*PV, error) {
-	return nil, nil
-}
-
-func (key *awsPVKey) Features() string {
-	storageClass := key.StorageClassName
-	if storageClass == "standard" {
-		storageClass = "pdstandard"
+	pricing, ok := aws.Pricing[pvk.Features()]
+	if !ok {
+		klog.V(2).Infof("Persistent Volume pricing not found for %s", pvk)
+		return &PV{}, nil
 	}
-	return key.Labels[v1.LabelZoneRegion] + storageClass
+	return pricing.PV, nil
 }
 
 type awsPVKey struct {
@@ -331,6 +374,17 @@ func (aws *AWS) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string)
 	}
 }
 
+func (key *awsPVKey) Features() string {
+	storageClass := key.StorageClassName
+	if storageClass == "standard" {
+		storageClass = "gp2"
+	}
+	// Storage class names are generally EBS volume types (gp2)
+	// Keys in Pricing are based on UsageTypes (EBS:VolumeType.gp2)
+	// Converts between the 2
+	return key.Labels[v1.LabelZoneRegion] + "," + volTypes[storageClass]
+}
+
 // GetKey maps node labels to information needed to retrieve pricing data
 func (aws *AWS) GetKey(labels map[string]string) Key {
 	return &awsKey{
@@ -385,6 +439,29 @@ func (aws *AWS) DownloadPricingData() error {
 		inputkeys[key.Features()] = true
 	}
 
+	pvList, err := aws.Clientset.CoreV1().PersistentVolumes().List(metav1.ListOptions{})
+	if err != nil {
+		return err
+	}
+
+	storageClasses, err := aws.Clientset.StorageV1().StorageClasses().List(metav1.ListOptions{})
+	storageClassMap := make(map[string]map[string]string)
+	for _, storageClass := range storageClasses.Items {
+		params := storageClass.Parameters
+		storageClassMap[storageClass.ObjectMeta.Name] = params
+	}
+
+	pvkeys := make(map[string]PVKey)
+	for _, pv := range pvList.Items {
+		params, ok := storageClassMap[pv.Spec.StorageClassName]
+		if !ok {
+			klog.Infof("Unable to find params for storageClassName %s", pv.Name)
+			continue
+		}
+		key := aws.GetPVKey(&pv, params)
+		pvkeys[key.Features()] = key
+	}
+
 	aws.Pricing = make(map[string]*AWSProductTerms)
 	aws.ValidPricingKeys = make(map[string]bool)
 	skusToKeys := make(map[string]string)
@@ -441,6 +518,27 @@ func (aws *AWS) DownloadPricingData() error {
 					}
 					aws.ValidPricingKeys[key] = true
 					aws.ValidPricingKeys[spotKey] = true
+				} else if strings.Contains(product.Attributes.UsageType, "EBS:Volume") {
+					key := locationToRegion[product.Attributes.Location] + "," + product.Attributes.UsageType
+					// UsageTypes may be prefixed with a region code - we're removing this when using
+					// volTypes to keep lookups generic
+					usageTypeRegx := regexp.MustCompile(".*(-|^)(EBS.+)")
+					usageTypeMatch := usageTypeRegx.FindStringSubmatch(product.Attributes.UsageType)
+					usageTypeNoRegion := usageTypeMatch[len(usageTypeMatch)-1]
+					spotKey := key + ",preemptible"
+					pv := &PV{
+						Class:      volTypes[usageTypeNoRegion],
+						Region:     locationToRegion[product.Attributes.Location],
+					}
+					productTerms := &AWSProductTerms{
+						Sku: product.Sku,
+						PV:  pv,
+					}
+					aws.Pricing[key] = productTerms
+					aws.Pricing[spotKey] = productTerms
+					skusToKeys[product.Sku] = key
+					aws.ValidPricingKeys[key] = true
+					aws.ValidPricingKeys[spotKey] = true
 				}
 			}
 		}
@@ -482,6 +580,30 @@ func (aws *AWS) DownloadPricingData() error {
 						if ok {
 							aws.Pricing[key].OnDemand = offerTerm
 							aws.Pricing[spotKey].OnDemand = offerTerm
+							if strings.Contains(key, "EBS:VolumeP-IOPS.piops") {
+								// If the specific UsageType is the per IO cost used on io1 volumes
+								// we need to add the per IO cost to the io1 PV cost
+								currRegion := strings.Split(key, ",")[0]
+								cost := offerTerm.PriceDimensions[sku.(string)+OnDemandRateCode+HourlyRateCode].PricePerUnit.USD
+								// UsageType in most regions starts with a region identifier
+								// Here we get that region identifier and then use it as part of
+								// the key for looking up and storing the per IO cost with the io1 price
+								billingRegionCode := regionToBillingRegionCode[currRegion]
+								piopsKey := ""
+								if billingRegionCode != "" {
+									piopsKey = currRegion + "," + billingRegionCode + "-EBS:VolumeUsage.piops"
+								} else {
+									piopsKey = currRegion + "," + "EBS:VolumeUsage.piops"
+								}
+								// Add the per IO cost to the PV object for the io1 volume type
+								aws.Pricing[piopsKey].PV.CostPerIO = cost
+							} else if strings.Contains(key, "EBS:Volume") {
+								// If volume, we need to get hourly cost and add it to the PV object
+								cost := offerTerm.PriceDimensions[sku.(string)+OnDemandRateCode+HourlyRateCode].PricePerUnit.USD
+								costFloat, _ := strconv.ParseFloat(cost, 64)
+								hourlyPrice := (costFloat * math.Pow10(-9)) / 730
+								aws.Pricing[key].PV.Cost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
+							}
 						}
 					}
 					_, err = dec.Token()

+ 1 - 0
cloud/provider.go

@@ -46,6 +46,7 @@ type Node struct {
 // The provider will best-effort try to fill out this struct.
 type PV struct {
 	Cost       string            `json:"hourlyCost"`
+	CostPerIO  string            `json:"costPerIOOperation"`
 	Class      string            `json:"storageClass"`
 	Size       string            `json:"size"`
 	Region     string            `json:"region"`