Merge branch 'main' into 19385-cwl-fast-pack
[arvados.git] / lib / cloud / ec2 / ec2.go
index f80e9bd1a52553421befdc4cc504382202328b55..b90eff6d571301085c2acd3d9bb8df56d1cfe54c 100644 (file)
@@ -48,6 +48,7 @@ type ec2InstanceSetConfig struct {
        SubnetID                string
        AdminUsername           string
        EBSVolumeType           string
+       EBSPrice                float64
        IAMInstanceProfile      string
        SpotPriceUpdateInterval arvados.Duration
 }
@@ -245,10 +246,6 @@ func (instanceSet *ec2InstanceSet) Create(
                }
        }
 
-       if instanceSet.ec2config.SpotPriceUpdateInterval <= 0 {
-               instanceSet.ec2config.SpotPriceUpdateInterval = arvados.Duration(24 * time.Hour)
-       }
-
        rsv, err := instanceSet.client.RunInstances(&rii)
        err = wrapError(err, &instanceSet.throttleDelayCreate)
        if err != nil {
@@ -295,14 +292,19 @@ func (instanceSet *ec2InstanceSet) Instances(tags cloud.InstanceTags) (instances
                }
                dii.NextToken = dio.NextToken
        }
-       if needAZs {
+       if needAZs && instanceSet.ec2config.SpotPriceUpdateInterval > 0 {
                az := map[string]string{}
-               instanceSet.client.DescribeInstanceStatusPages(&ec2.DescribeInstanceStatusInput{}, func(page *ec2.DescribeInstanceStatusOutput, lastPage bool) bool {
+               err := instanceSet.client.DescribeInstanceStatusPages(&ec2.DescribeInstanceStatusInput{
+                       IncludeAllInstances: aws.Bool(true),
+               }, func(page *ec2.DescribeInstanceStatusOutput, lastPage bool) bool {
                        for _, ent := range page.InstanceStatuses {
                                az[*ent.InstanceId] = *ent.AvailabilityZone
                        }
                        return true
                })
+               if err != nil {
+                       instanceSet.logger.Warnf("error getting instance statuses: %s", err)
+               }
                for _, inst := range instances {
                        inst := inst.(*ec2Instance)
                        inst.availabilityZone = az[*inst.instance.InstanceId]
@@ -335,7 +337,8 @@ func (instanceSet *ec2InstanceSet) updateSpotPrices(instances []cloud.Instance)
        updateTime := time.Now()
        staleTime := updateTime.Add(-instanceSet.ec2config.SpotPriceUpdateInterval.Duration())
        needUpdate := false
-       var typeFilterValues []*string
+       allTypes := map[string]bool{}
+
        for _, inst := range instances {
                ec2inst := inst.(*ec2Instance).instance
                if aws.StringValue(ec2inst.InstanceLifecycle) == "spot" {
@@ -347,12 +350,16 @@ func (instanceSet *ec2InstanceSet) updateSpotPrices(instances []cloud.Instance)
                        if instanceSet.pricesUpdated[pk].Before(staleTime) {
                                needUpdate = true
                        }
-                       typeFilterValues = append(typeFilterValues, ec2inst.InstanceType)
+                       allTypes[*ec2inst.InstanceType] = true
                }
        }
        if !needUpdate {
                return
        }
+       var typeFilterValues []*string
+       for instanceType := range allTypes {
+               typeFilterValues = append(typeFilterValues, aws.String(instanceType))
+       }
        // Get 3x update interval worth of pricing data. (Ideally the
        // AWS API would tell us "we have shown you all of the price
        // changes up to time T", but it doesn't, so we'll just ask
@@ -363,7 +370,8 @@ func (instanceSet *ec2InstanceSet) updateSpotPrices(instances []cloud.Instance)
        dsphi := &ec2.DescribeSpotPriceHistoryInput{
                StartTime: aws.Time(updateTime.Add(-3 * instanceSet.ec2config.SpotPriceUpdateInterval.Duration())),
                Filters: []*ec2.Filter{
-                       &ec2.Filter{Name: aws.String("InstanceType"), Values: typeFilterValues},
+                       &ec2.Filter{Name: aws.String("instance-type"), Values: typeFilterValues},
+                       &ec2.Filter{Name: aws.String("product-description"), Values: []*string{aws.String("Linux/UNIX")}},
                },
        }
        err := instanceSet.client.DescribeSpotPriceHistoryPages(dsphi, func(page *ec2.DescribeSpotPriceHistoryOutput, lastPage bool) bool {
@@ -500,14 +508,27 @@ func (inst *ec2Instance) VerifyHostKey(ssh.PublicKey, *ssh.Client) error {
 // Spot price that is in effect when your Spot Instance is running."
 // (The use of the phrase "is running", as opposed to "was launched",
 // hints that pricing is dynamic.)
-func (inst *ec2Instance) PriceHistory() []cloud.InstancePrice {
+func (inst *ec2Instance) PriceHistory(instType arvados.InstanceType) []cloud.InstancePrice {
        inst.provider.pricesLock.Lock()
        defer inst.provider.pricesLock.Unlock()
-       return inst.provider.prices[priceKey{
+       // Note updateSpotPrices currently populates
+       // inst.provider.prices only for spot instances, so if
+       // spot==false here, we will return no data.
+       pk := priceKey{
                instanceType:     *inst.instance.InstanceType,
                spot:             aws.StringValue(inst.instance.InstanceLifecycle) == "spot",
                availabilityZone: inst.availabilityZone,
-       }]
+       }
+       var prices []cloud.InstancePrice
+       for _, price := range inst.provider.prices[pk] {
+               // ceil(added scratch space in GiB)
+               gib := (instType.AddedScratch + 1<<30 - 1) >> 30
+               monthly := inst.provider.ec2config.EBSPrice * float64(gib)
+               hourly := monthly / 30 / 24
+               price.Price += hourly
+               prices = append(prices, price)
+       }
+       return prices
 }
 
 type rateLimitError struct {