diff --git a/README.md b/README.md index b4daa88..59c93cf 100644 --- a/README.md +++ b/README.md @@ -79,25 +79,36 @@ $ export AWS_REGION="us-east-1" ``` $ ec2-instance-selector --memory 4 --vcpus 2 --cpu-architecture x86_64 -r us-east-1 c5.large +c5a.large +c5ad.large c5d.large t2.medium t3.medium t3a.medium ``` -**Find instance types that support 100GB/s networking** +**Find instance types that support 100GB/s networking that can be purchased as spot instances** ``` -$ ec2-instance-selector --network-performance 100 -r us-east-1 +$ ec2-instance-selector --network-performance 100 --usage-class spot -r us-east-1 c5n.18xlarge c5n.metal +c6gn.16xlarge +g4dn.metal i3en.24xlarge i3en.metal inf1.24xlarge m5dn.24xlarge +m5dn.metal m5n.24xlarge +m5n.metal +m5zn.12xlarge +m5zn.metal p3dn.24xlarge +p4d.24xlarge r5dn.24xlarge +r5dn.metal r5n.24xlarge +r5n.metal ``` **Short Table Output** @@ -105,24 +116,27 @@ r5n.24xlarge $ ec2-instance-selector --memory 4 --vcpus 2 --cpu-architecture x86_64 -r us-east-1 -o table Instance Type VCPUs Mem (GiB) ------------- ----- --------- -c5.large 2 4.000 -c5d.large 2 4.000 -t2.medium 2 4.000 -t3.medium 2 4.000 -t3a.medium 2 4.000 +c5.large 2 4 +c5a.large 2 4 +c5ad.large 2 4 +c5d.large 2 4 +t2.medium 2 4 +t3.medium 2 4 +t3a.medium 2 4 ``` **Wide Table Output** ``` $ ec2-instance-selector --memory 4 --vcpus 2 --cpu-architecture x86_64 -r us-east-1 -o table-wide -Instance Type VCPUs Mem (GiB) Hypervisor Current Gen Hibernation Support CPU Arch Network Performance ENIs GPUs -------------- ----- --------- ---------- ----------- ------------------- -------- ------------------- ---- ---- -c5.large 2 4.000 nitro true true x86_64 Up to 10 Gigabit 3 0 -c5a.large 2 4.000 nitro true false x86_64 Up to 10 Gigabit 3 0 -c5d.large 2 4.000 nitro true false x86_64 Up to 10 Gigabit 3 0 -t2.medium 2 4.000 xen true true i386, x86_64 Low to Moderate 3 0 -t3.medium 2 4.000 nitro true false x86_64 Up to 5 Gigabit 3 0 -t3a.medium 2 4.000 nitro true false x86_64 Up to 5 Gigabit 3 0 +Instance Type VCPUs Mem (GiB) Hypervisor Current Gen Hibernation Support CPU Arch Network Performance ENIs GPUs GPU Mem (GiB) GPU Info On-Demand Price/Hr +------------- ----- --------- ---------- ----------- ------------------- -------- ------------------- ---- ---- ------------- -------- ------------------ +c5.large 2 4 nitro true true x86_64 Up to 10 Gigabit 3 0 0 -No Price Filter Specified- +c5a.large 2 4 nitro true false x86_64 Up to 10 Gigabit 3 0 0 -No Price Filter Specified- +c5ad.large 2 4 nitro true false x86_64 Up to 10 Gigabit 3 0 0 -No Price Filter Specified- +c5d.large 2 4 nitro true false x86_64 Up to 10 Gigabit 3 0 0 -No Price Filter Specified- +t2.medium 2 4 xen true true i386, x86_64 Low to Moderate 3 0 0 -No Price Filter Specified- +t3.medium 2 4 nitro true true x86_64 Up to 5 Gigabit 3 0 0 -No Price Filter Specified- +t3a.medium 2 4 nitro true true x86_64 Up to 5 Gigabit 3 0 0 -No Price Filter Specified- ``` **All CLI Options** @@ -171,23 +185,28 @@ Filter Flags: --network-performance-max int Maximum Bandwidth in Gib/s of network performance (Example: 100) If --network-performance-min is not specified, the lower bound will be 0 --network-performance-min int Minimum Bandwidth in Gib/s of network performance (Example: 100) If --network-performance-max is not specified, the upper bound will be infinity --placement-group-strategy string Placement group strategy: [cluster, partition, spread] + --price-per-hour float Price/hour in USD (Example: 0.09) (sets --price-per-hour-min and -max to the same value) + --price-per-hour-max float Maximum Price/hour in USD (Example: 0.09) If --price-per-hour-min is not specified, the lower bound will be 0 + --price-per-hour-min float Minimum Price/hour in USD (Example: 0.09) If --price-per-hour-max is not specified, the upper bound will be infinity --root-device-type string Supported root device types: [ebs or instance-store] -u, --usage-class string Usage class: [spot or on-demand] -c, --vcpus int Number of vcpus available to the instance type. (sets --vcpus-min and -max to the same value) --vcpus-max int Maximum Number of vcpus available to the instance type. If --vcpus-min is not specified, the lower bound will be 0 --vcpus-min int Minimum Number of vcpus available to the instance type. If --vcpus-max is not specified, the upper bound will be infinity --vcpus-to-memory-ratio string The ratio of vcpus to GiBs of memory. (Example: 1:2) + --virtualization-type string Virtualization Type supported: [hvm or pv] Suite Flags: --base-instance-type string Instance Type used to retrieve similarly spec'd instance types --flexible Retrieves a group of instance types spanning multiple generations based on opinionated defaults and user overridden resource filters + --service string Filter instance types based on service support (Example: eks, eks-20201211, or emr-5.20.0) Global Flags: -h, --help Help --max-results int The maximum number of instance types that match your criteria to return (default 20) - -o, --output string Specify the output format (table, table-wide) + -o, --output string Specify the output format (table, table-wide, one-line) --profile string AWS CLI profile to use for credentials and config -r, --region string AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence) -v, --verbose Verbose - will print out full instance specs @@ -267,7 +286,7 @@ func main() { $ git clone https://github.com/aws/amazon-ec2-instance-selector.git $ cd amazon-ec2-instance-selector/ $ go run cmd/examples/example1.go -[c1.medium c3.large c4.large c5.large c5d.large t2.medium t3.medium t3.micro t3.small t3a.medium t3a.micro t3a.small] +[c4.large c5.large c5a.large c5d.large t2.medium t3.medium t3.small t3a.medium t3a.small] ``` ## Building diff --git a/cmd/main.go b/cmd/main.go index bbf427a..e345bac 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -69,6 +69,7 @@ const ( allowList = "allow-list" denyList = "deny-list" virtualizationType = "virtualization-type" + pricePerHour = "price-per-hour" ) // Aggregate Filter Flags @@ -142,6 +143,7 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.RegexFlag(allowList, nil, nil, "List of allowed instance types to select from w/ regex syntax (Example: m[3-5]\\.*)") cli.RegexFlag(denyList, nil, nil, "List of instance types which should be excluded w/ regex syntax (Example: m[1-2]\\.*)") cli.StringOptionsFlag(virtualizationType, nil, nil, "Virtualization Type supported: [hvm or pv]", []string{"hvm", "paravirtual", "pv"}) + cli.Float64MinMaxRangeFlags(pricePerHour, nil, nil, "Price/hour in USD (Example: 0.09)") // Suite Flags - higher level aggregate filters that return opinionated result @@ -183,6 +185,13 @@ Full docs can be found at github.com/aws/amazon-` + binName flags[region] = sess.Config.Region instanceSelector := selector.New(sess) + if _, ok := flags[pricePerHour]; ok { + if flags[usageClass] == nil || *flags[usageClass].(*string) == "on-demand" { + instanceSelector.EC2Pricing.HydrateOndemandCache() + } else { + instanceSelector.EC2Pricing.HydrateSpotCache(30) + } + } filters := selector.Filters{ VCpusRange: cli.IntRangeMe(flags[vcpus]), @@ -212,6 +221,7 @@ Full docs can be found at github.com/aws/amazon-` + binName Flexible: cli.BoolMe(flags[flexible]), Service: cli.StringMe(flags[service]), VirtualizationType: cli.StringMe(flags[virtualizationType]), + PricePerHour: cli.Float64RangeMe(flags[pricePerHour]), } if flags[verbose] != nil { diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index b360f39..a86d325 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -16,6 +16,7 @@ package cli import ( "fmt" + "math" "os" "reflect" "strings" @@ -185,6 +186,10 @@ func (cl *CommandLineInterface) SetUntouchedFlagValuesToNil() error { if v.Quantity == 0 { cl.Flags[f.Name] = nil } + case *float64: + if reflect.ValueOf(*v).IsZero() { + cl.Flags[f.Name] = nil + } case *string: if reflect.ValueOf(*v).IsZero() { cl.Flags[f.Name] = nil @@ -231,6 +236,8 @@ func (cl *CommandLineInterface) ProcessRangeFilterFlags() error { cl.Flags[rangeHelperMin] = cl.IntMe(0) case *bytequantity.ByteQuantity: cl.Flags[rangeHelperMin] = cl.ByteQuantityMe(bytequantity.ByteQuantity{Quantity: 0}) + case *float64: + cl.Flags[rangeHelperMin] = cl.Float64Me(0.0) default: return fmt.Errorf("Unable to set %s", rangeHelperMax) } @@ -240,6 +247,8 @@ func (cl *CommandLineInterface) ProcessRangeFilterFlags() error { cl.Flags[rangeHelperMax] = cl.IntMe(maxInt) case *bytequantity.ByteQuantity: cl.Flags[rangeHelperMax] = cl.ByteQuantityMe(bytequantity.ByteQuantity{Quantity: maxUint64}) + case *float64: + cl.Flags[rangeHelperMax] = cl.Float64Me(math.MaxFloat64) default: return fmt.Errorf("Unable to set %s", rangeHelperMin) } @@ -256,6 +265,11 @@ func (cl *CommandLineInterface) ProcessRangeFilterFlags() error { LowerBound: *cl.ByteQuantityMe(cl.Flags[rangeHelperMin]), UpperBound: *cl.ByteQuantityMe(cl.Flags[rangeHelperMax]), } + case *float64: + cl.Flags[flagName] = &selector.Float64RangeFilter{ + LowerBound: *cl.Float64Me(cl.Flags[rangeHelperMin]), + UpperBound: *cl.Float64Me(cl.Flags[rangeHelperMax]), + } } } return nil diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index ea334ff..a3ed253 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -159,7 +159,7 @@ func TestParseFlags_IntRange(t *testing.T) { h.Ok(t, err) flagMinOutput = flags[flagMinArg].(*int) flagMaxOutput = flags[flagMaxArg].(*int) - h.Assert(t, *flagMinOutput == 10 && *flagMaxOutput == 500, "Flag %s max should have been parsed from cmdline and min set to 0", flagArg) + h.Assert(t, *flagMinOutput == 10 && *flagMaxOutput == 500, "Flag %s min and max should have been parsed from cmdline", flagArg) } func TestParseFlags_IntRangeErr(t *testing.T) { @@ -222,6 +222,53 @@ func TestParseFlags_ByteQuantityRange(t *testing.T) { h.Assert(t, flagType == bqRangeFilterType, "%s should be of type %v, instead got %v", flagArg, bqRangeFilterType, flagType) } +func TestParseFlags_Float64Range(t *testing.T) { + flagName := "test-flag" + flagMinArg := fmt.Sprintf("%s-%s", flagName, "min") + flagMaxArg := fmt.Sprintf("%s-%s", flagName, "max") + flagArg := fmt.Sprintf("--%s", flagName) + + // Root set Min and Max to the same val + cli := getTestCLI() + cli.Float64MinMaxRangeFlags(flagName, nil, nil, "Test") + os.Args = []string{"ec2-instance-selector", flagArg, "5.1"} + flags, err := cli.ParseFlags() + h.Ok(t, err) + flagMinOutput := flags[flagMinArg].(*float64) + flagMaxOutput := flags[flagMaxArg].(*float64) + h.Assert(t, *flagMinOutput == 5.1 && *flagMaxOutput == 5.1, "Flag %s min and max should have been parsed to the same number", flagArg) + + // Min is set to a val and max is set to maxInt + cli = getTestCLI() + cli.Float64MinMaxRangeFlags(flagName, nil, nil, "Test") + os.Args = []string{"ec2-instance-selector", "--" + flagMinArg, "5.1"} + flags, err = cli.ParseFlags() + h.Ok(t, err) + flagMinOutput = flags[flagMinArg].(*float64) + flagMaxOutput = flags[flagMaxArg].(*float64) + h.Assert(t, *flagMinOutput == 5.1 && *flagMaxOutput == math.MaxFloat64, "Flag %s min should have been parsed from cmdline and max set to math.MaxFloat64", flagArg) + + // Max is set to a val and min is set to 0 + cli = getTestCLI() + cli.Float64MinMaxRangeFlags(flagName, nil, nil, "Test") + os.Args = []string{"ec2-instance-selector", "--" + flagMaxArg, "5.1"} + flags, err = cli.ParseFlags() + h.Ok(t, err) + flagMinOutput = flags[flagMinArg].(*float64) + flagMaxOutput = flags[flagMaxArg].(*float64) + h.Assert(t, *flagMinOutput == 0.0 && *flagMaxOutput == 5.1, "Flag %s max should have been parsed from cmdline and min set to 0.0", flagArg) + + // Min and Max are set to separate values + cli = getTestCLI() + cli.Float64MinMaxRangeFlags(flagName, nil, nil, "Test") + os.Args = []string{"ec2-instance-selector", "--" + flagMinArg, "10.1", "--" + flagMaxArg, "500.1"} + flags, err = cli.ParseFlags() + h.Ok(t, err) + flagMinOutput = flags[flagMinArg].(*float64) + flagMaxOutput = flags[flagMaxArg].(*float64) + h.Assert(t, *flagMinOutput == 10.1 && *flagMaxOutput == 500.1, "Flag %s min and max should have been parsed from cmdline", flagArg) +} + func TestParseAndValidateFlags_ByteQuantityRange(t *testing.T) { flagName := "test-flag" flagMinArg := fmt.Sprintf("%s-%s", flagName, "min") diff --git a/pkg/cli/flags.go b/pkg/cli/flags.go index 0506971..fe5b1bc 100644 --- a/pkg/cli/flags.go +++ b/pkg/cli/flags.go @@ -59,6 +59,11 @@ func (cl *CommandLineInterface) ByteQuantityMinMaxRangeFlags(name string, shorth cl.ByteQuantityMinMaxRangeFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description) } +// Float64MinMaxRangeFlags creates and registers a min, max, and helper flag each accepting a float64 +func (cl *CommandLineInterface) Float64MinMaxRangeFlags(name string, shorthand *string, defaultValue *float64, description string) { + cl.Float64MinMaxRangeFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description) +} + // ByteQuantityFlag creates and registers a flag accepting a byte quantity like 512mb func (cl *CommandLineInterface) ByteQuantityFlag(name string, shorthand *string, defaultValue *bytequantity.ByteQuantity, description string) { cl.ByteQuantityFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description) @@ -183,6 +188,27 @@ func (cl *CommandLineInterface) IntMinMaxRangeFlagOnFlagSet(flagSet *pflag.FlagS cl.rangeFlags[name] = true } +// Float64MinMaxRangeFlagOnFlagSet creates and registers a min, max, and helper flag each accepting a Float64 +func (cl *CommandLineInterface) Float64MinMaxRangeFlagOnFlagSet(flagSet *pflag.FlagSet, name string, shorthand *string, defaultValue *float64, description string) { + cl.Float64FlagOnFlagSet(flagSet, name, shorthand, defaultValue, fmt.Sprintf("%s (sets --%s-min and -max to the same value)", description, name)) + cl.Float64FlagOnFlagSet(flagSet, name+"-min", nil, nil, fmt.Sprintf("Minimum %s If --%s-max is not specified, the upper bound will be infinity", description, name)) + cl.Float64FlagOnFlagSet(flagSet, name+"-max", nil, nil, fmt.Sprintf("Maximum %s If --%s-min is not specified, the lower bound will be 0", description, name)) + cl.validators[name] = func(val interface{}) error { + if cl.Flags[name+"-min"] == nil || cl.Flags[name+"-max"] == nil { + return nil + } + minArg := name + "-min" + maxArg := name + "-max" + minVal := cl.Flags[minArg].(*float64) + maxVal := cl.Flags[maxArg].(*float64) + if *minVal > *maxVal { + return fmt.Errorf("Invalid input for --%s and --%s. %s must be less than or equal to %s", minArg, maxArg, minArg, maxArg) + } + return nil + } + cl.rangeFlags[name] = true +} + // ByteQuantityMinMaxRangeFlagOnFlagSet creates and registers a min, max, and helper flag each accepting a ByteQuantity like 5mb or 12gb func (cl *CommandLineInterface) ByteQuantityMinMaxRangeFlagOnFlagSet(flagSet *pflag.FlagSet, name string, shorthand *string, defaultValue *bytequantity.ByteQuantity, description string) { cl.ByteQuantityFlagOnFlagSet(flagSet, name, shorthand, defaultValue, fmt.Sprintf("%s (sets --%s-min and -max to the same value)", description, name)) @@ -258,6 +284,19 @@ func (cl *CommandLineInterface) IntFlagOnFlagSet(flagSet *pflag.FlagSet, name st cl.Flags[name] = flagSet.Int(name, *defaultValue, description) } +// Float64FlagOnFlagSet creates and registers a flag accepting a Float64 +func (cl *CommandLineInterface) Float64FlagOnFlagSet(flagSet *pflag.FlagSet, name string, shorthand *string, defaultValue *float64, description string) { + if defaultValue == nil { + cl.nilDefaults[name] = true + defaultValue = cl.Float64Me(0.0) + } + if shorthand != nil { + cl.Flags[name] = flagSet.Float64P(name, string(*shorthand), *defaultValue, description) + return + } + cl.Flags[name] = flagSet.Float64(name, *defaultValue, description) +} + // StringFlagOnFlagSet creates and registers a flag accepting a String and a validator function. // The validator function is provided so that more complex flags can be created from a string input. func (cl *CommandLineInterface) StringFlagOnFlagSet(flagSet *pflag.FlagSet, name string, shorthand *string, defaultValue *string, description string, processorFn processor, validationFn validator) { diff --git a/pkg/cli/flags_test.go b/pkg/cli/flags_test.go index 94c7c9a..ab737f9 100644 --- a/pkg/cli/flags_test.go +++ b/pkg/cli/flags_test.go @@ -201,3 +201,21 @@ func TestRegexFlag(t *testing.T) { h.Assert(t, ok, "Should contain %s flag w/ no shorthand", flagName) } } + +func TestFloat64MinMaxRangeFlags(t *testing.T) { + cli := getTestCLI() + flagName := "test-float64-min-max-range" + cli.Float64MinMaxRangeFlags(flagName, cli.StringMe("t"), nil, "Test Min Max Range") + _, ok := cli.Flags[flagName] + _, minOk := cli.Flags[flagName+"-min"] + _, maxOk := cli.Flags[flagName+"-max"] + h.Assert(t, len(cli.Flags) == 3, "Should contain 3 flags") + h.Assert(t, ok, "Should contain %s flag", flagName) + h.Assert(t, minOk, "Should contain %s flag", flagName) + h.Assert(t, maxOk, "Should contain %s flag", flagName) + + cli = getTestCLI() + cli.Float64MinMaxRangeFlags(flagName, nil, nil, "Test Min Max Range") + h.Assert(t, len(cli.Flags) == 3, "Should contain 3 flags w/ no shorthand") + h.Assert(t, ok, "Should contain %s flag w/ no shorthand", flagName) +} diff --git a/pkg/cli/types.go b/pkg/cli/types.go index f54f515..d2d059a 100644 --- a/pkg/cli/types.go +++ b/pkg/cli/types.go @@ -139,6 +139,23 @@ func (*CommandLineInterface) ByteQuantityRangeMe(i interface{}) *selector.ByteQu } } +// Float64RangeMe takes an interface and returns a pointer to a Float64RangeFilter value +// If the underlying interface kind is not Float64RangeFilter or *Float64RangeFilter then nil is returned +func (*CommandLineInterface) Float64RangeMe(i interface{}) *selector.Float64RangeFilter { + if i == nil { + return nil + } + switch v := i.(type) { + case *selector.Float64RangeFilter: + return v + case selector.Float64RangeFilter: + return &v + default: + log.Printf("%s cannot be converted to a Float64Range", i) + return nil + } +} + // StringMe takes an interface and returns a pointer to a string value // If the underlying interface kind is not string or *string then nil is returned func (*CommandLineInterface) StringMe(i interface{}) *string { diff --git a/pkg/cli/types_test.go b/pkg/cli/types_test.go index f83c8b7..20ebaef 100644 --- a/pkg/cli/types_test.go +++ b/pkg/cli/types_test.go @@ -134,3 +134,16 @@ func TestRegexMe(t *testing.T) { val = cli.RegexMe(nil) h.Assert(t, val == nil, "Should return nil if nil is passed in") } + +func TestFloat64RangeMe(t *testing.T) { + cli := getTestCLI() + float64RangeVal := selector.Float64RangeFilter{LowerBound: 1.0, UpperBound: 2.1} + val := cli.Float64RangeMe(float64RangeVal) + h.Assert(t, *val == float64RangeVal, "Should return %s from passed in float64 range value", float64RangeVal) + val = cli.Float64RangeMe(&float64RangeVal) + h.Assert(t, *val == float64RangeVal, "Should return %s from passed in range pointer", float64RangeVal) + val = cli.Float64RangeMe(true) + h.Assert(t, val == nil, "Should return nil from other data type passed in") + val = cli.Float64RangeMe(nil) + h.Assert(t, val == nil, "Should return nil if nil is passed in") +} diff --git a/pkg/ec2pricing/ec2pricing.go b/pkg/ec2pricing/ec2pricing.go new file mode 100644 index 0000000..b437ac1 --- /dev/null +++ b/pkg/ec2pricing/ec2pricing.go @@ -0,0 +1,312 @@ +package ec2pricing + +import ( + "fmt" + "math" + "sort" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go/service/pricing" + "github.com/aws/aws-sdk-go/service/pricing/pricingiface" +) + +const ( + defaultSpotDaysBack = 30 + productDescription = "Linux/UNIX (Amazon VPC)" + serviceCode = "AmazonEC2" +) + +// EC2Pricing is the public struct to interface with AWS pricing APIs +type EC2Pricing struct { + PricingClient pricingiface.PricingAPI + EC2Client ec2iface.EC2API + AWSSession *session.Session + cache map[string]float64 + spotCache map[string]map[string][]spotPricingEntry +} + +// EC2PricingIface is the EC2Pricing interface mainly used to mock out ec2pricing during testing +type EC2PricingIface interface { + GetOndemandInstanceTypeCost(instanceType string) (float64, error) + GetSpotInstanceTypeNDayAvgCost(instanceType string, availabilityZones []string, days int) (float64, error) + HydrateOndemandCache() error + HydrateSpotCache(days int) error +} + +type spotPricingEntry struct { + Timestamp time.Time + SpotPrice float64 +} + +// New creates an instance of instance-selector EC2Pricing +func New(sess *session.Session) *EC2Pricing { + return &EC2Pricing{ + PricingClient: pricing.New(sess), + EC2Client: ec2.New(sess), + AWSSession: sess, + } +} + +// GetSpotInstanceTypeNDayAvgCost retrieves the spot price history for a given AZ from the past N days and averages the price +// Passing an empty list for availabilityZones will retrieve avg cost for all AZs in the current AWSSession's region +func (p *EC2Pricing) GetSpotInstanceTypeNDayAvgCost(instanceType string, availabilityZones []string, days int) (float64, error) { + endTime := time.Now().UTC() + startTime := endTime.Add(time.Hour * time.Duration(24*-1*days)) + + spotPriceHistInput := ec2.DescribeSpotPriceHistoryInput{ + ProductDescriptions: []*string{aws.String(productDescription)}, + StartTime: &startTime, + EndTime: &endTime, + InstanceTypes: []*string{&instanceType}, + } + zoneToPriceEntries := map[string][]spotPricingEntry{} + + if _, ok := p.spotCache[instanceType]; !ok { + var processingErr error + err := p.EC2Client.DescribeSpotPriceHistoryPages(&spotPriceHistInput, func(dspho *ec2.DescribeSpotPriceHistoryOutput, b bool) bool { + for _, history := range dspho.SpotPriceHistory { + var spotPrice float64 + spotPrice, processingErr = strconv.ParseFloat(*history.SpotPrice, 64) + zone := *history.AvailabilityZone + + zoneToPriceEntries[zone] = append(zoneToPriceEntries[zone], spotPricingEntry{ + Timestamp: *history.Timestamp, + SpotPrice: spotPrice, + }) + } + return true + }) + if err != nil { + return float64(0), err + } + if processingErr != nil { + return float64(0), processingErr + } + } else { + for zone, priceEntries := range p.spotCache[instanceType] { + for _, entry := range priceEntries { + zoneToPriceEntries[zone] = append(zoneToPriceEntries[zone], spotPricingEntry{ + Timestamp: entry.Timestamp, + SpotPrice: entry.SpotPrice, + }) + } + } + } + + aggregateZoneSum := float64(0) + numOfZones := 0 + for zone, priceEntries := range zoneToPriceEntries { + if len(availabilityZones) != 0 { + if !strings.Contains(strings.Join(availabilityZones, " "), zone) { + continue + } + } + numOfZones++ + aggregateZoneSum += p.calculateSpotAggregate(priceEntries) + } + + return (aggregateZoneSum / float64(numOfZones)), nil +} + +func (p *EC2Pricing) calculateSpotAggregate(spotPriceEntries []spotPricingEntry) float64 { + if len(spotPriceEntries) == 0 { + return 0.0 + } + // Sort slice by timestamp in decending order from the end time (most likely, now) + sort.Slice(spotPriceEntries, func(i, j int) bool { + return spotPriceEntries[i].Timestamp.After(spotPriceEntries[j].Timestamp) + }) + + endTime := spotPriceEntries[0].Timestamp + startTime := spotPriceEntries[len(spotPriceEntries)-1].Timestamp + totalDuration := endTime.Sub(startTime).Minutes() + + priceSum := float64(0) + for i, entry := range spotPriceEntries { + duration := spotPriceEntries[int(math.Max(float64(i-1), 0))].Timestamp.Sub(entry.Timestamp).Minutes() + priceSum += duration * entry.SpotPrice + } + return (priceSum / totalDuration) +} + +// GetOndemandInstanceTypeCost retrieves the on-demand hourly cost for the specified instance type +func (p *EC2Pricing) GetOndemandInstanceTypeCost(instanceType string) (float64, error) { + regionDescription := p.getRegionForPricingAPI() + // TODO: mac.metal instances cannot be found with the below filters + productInput := pricing.GetProductsInput{ + ServiceCode: aws.String(serviceCode), + Filters: []*pricing.Filter{ + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("ServiceCode"), Value: aws.String(serviceCode)}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("operatingSystem"), Value: aws.String("linux")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("location"), Value: aws.String(regionDescription)}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("capacitystatus"), Value: aws.String("used")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("preInstalledSw"), Value: aws.String("NA")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("tenancy"), Value: aws.String("shared")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("instanceType"), Value: aws.String(instanceType)}, + }, + } + + // Check cache first and return it if available + if price, ok := p.cache[instanceType]; ok { + return price, nil + } + + pricePerUnitInUSD := float64(-1) + err := p.PricingClient.GetProductsPages(&productInput, func(pricingOutput *pricing.GetProductsOutput, nextPage bool) bool { + var err error + for _, priceDoc := range pricingOutput.PriceList { + _, pricePerUnitInUSD, err = parseOndemandUnitPrice(priceDoc) + } + if err != nil { + // keep going through pages if we can't parse the pricing doc + return true + } + return false + }) + if err != nil { + return -1, err + } + return pricePerUnitInUSD, nil +} + +// HydrateSpotCache makes a bulk request to the spot-pricing-history api to retrieve all instance type pricing and stores them in a local cache +// If HydrateSpotCache is called more than once, the cache will be fully refreshed +// There is no TTL on cache entries +// You'll only want to use this if you don't mind a long startup time (around 30 seconds) and will query the cache often after that. +func (p *EC2Pricing) HydrateSpotCache(days int) error { + newCache := map[string]map[string][]spotPricingEntry{} + + endTime := time.Now().UTC() + startTime := endTime.Add(time.Hour * time.Duration(24*-1*days)) + spotPriceHistInput := ec2.DescribeSpotPriceHistoryInput{ + ProductDescriptions: []*string{aws.String(productDescription)}, + StartTime: &startTime, + EndTime: &endTime, + } + var processingErr error + err := p.EC2Client.DescribeSpotPriceHistoryPages(&spotPriceHistInput, func(dspho *ec2.DescribeSpotPriceHistoryOutput, b bool) bool { + for _, history := range dspho.SpotPriceHistory { + var spotPrice float64 + spotPrice, processingErr = strconv.ParseFloat(*history.SpotPrice, 64) + instanceType := *history.InstanceType + zone := *history.AvailabilityZone + if _, ok := newCache[instanceType]; !ok { + newCache[instanceType] = map[string][]spotPricingEntry{} + } + newCache[instanceType][zone] = append(newCache[instanceType][zone], spotPricingEntry{ + Timestamp: *history.Timestamp, + SpotPrice: spotPrice, + }) + } + return true + }) + if err != nil { + return err + } + p.spotCache = newCache + return processingErr +} + +// HydrateOndemandCache makes a bulk request to the pricing api to retrieve all instance type pricing and stores them in a local cache +// If HydrateOndemandCache is called more than once, the cache will be fully refreshed +// There is no TTL on cache entries +func (p *EC2Pricing) HydrateOndemandCache() error { + if p.cache == nil { + p.cache = make(map[string]float64) + } + regionDescription := p.getRegionForPricingAPI() + productInput := pricing.GetProductsInput{ + ServiceCode: aws.String(serviceCode), + Filters: []*pricing.Filter{ + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("ServiceCode"), Value: aws.String(serviceCode)}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("operatingSystem"), Value: aws.String("linux")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("location"), Value: aws.String(regionDescription)}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("capacitystatus"), Value: aws.String("used")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("preInstalledSw"), Value: aws.String("NA")}, + {Type: aws.String(pricing.FilterTypeTermMatch), Field: aws.String("tenancy"), Value: aws.String("shared")}, + }, + } + err := p.PricingClient.GetProductsPages(&productInput, func(pricingOutput *pricing.GetProductsOutput, nextPage bool) bool { + for _, priceDoc := range pricingOutput.PriceList { + instanceTypeName, price, err := parseOndemandUnitPrice(priceDoc) + if err != nil { + continue + } + p.cache[instanceTypeName] = price + } + return true + }) + return err +} + +// getRegionForPricingAPI attempts to retrieve the region description based on the AWS session used to create +// the ec2pricing struct. It then uses the endpoints package in the aws sdk to retrieve the region description +// This is necessary because the pricing API uses the region description rather than a region ID +func (p *EC2Pricing) getRegionForPricingAPI() string { + endpointResolver := endpoints.DefaultResolver() + partitions := endpointResolver.(endpoints.EnumPartitions).Partitions() + + // use us-east-1 as the default + regionDescription := "US East (N. Virginia)" + for _, partition := range partitions { + regions := partition.Regions() + if region, ok := regions[*p.AWSSession.Config.Region]; ok { + regionDescription = region.Description() + } + } + return regionDescription +} + +// parseOndemandUnitPrice takes a priceList from the pricing API and parses its weirdness +func parseOndemandUnitPrice(priceList aws.JSONValue) (string, float64, error) { + // TODO: this could probably be cleaned up a bit by adding a couple structs with json tags + // We still need to some weird for-loops to get at elements under json keys that are IDs... + // But it would probably be cleaner than this. + attributes, ok := priceList["product"].(map[string]interface{})["attributes"] + if !ok { + return "", float64(-1.0), fmt.Errorf("Unable to find product attributes") + } + instanceTypeName, ok := attributes.(map[string]interface{})["instanceType"].(string) + if !ok { + return "", float64(-1.0), fmt.Errorf("Unable to find instance type name from product attributes") + } + terms, ok := priceList["terms"] + if !ok { + return instanceTypeName, float64(-1.0), fmt.Errorf("Unable to find pricing terms") + } + ondemandTerms, ok := terms.(map[string]interface{})["OnDemand"] + if !ok { + return instanceTypeName, float64(-1.0), fmt.Errorf("Unable to find on-demand pricing terms") + } + for _, priceDimensions := range ondemandTerms.(map[string]interface{}) { + dim, ok := priceDimensions.(map[string]interface{})["priceDimensions"] + if !ok { + return instanceTypeName, float64(-1.0), fmt.Errorf("Unable to find on-demand pricing dimensions") + } + for _, dimension := range dim.(map[string]interface{}) { + dims := dimension.(map[string]interface{}) + pricePerUnit, ok := dims["pricePerUnit"] + if !ok { + return instanceTypeName, float64(-1.0), fmt.Errorf("Unable to find on-demand price per unit in pricing dimensions") + } + pricePerUnitInUSDStr, ok := pricePerUnit.(map[string]interface{})["USD"] + if !ok { + return instanceTypeName, float64(-1.0), fmt.Errorf("Unable to find on-demand price per unit in USD") + } + var err error + pricePerUnitInUSD, err := strconv.ParseFloat(pricePerUnitInUSDStr.(string), 64) + if err != nil { + return instanceTypeName, float64(-1.0), fmt.Errorf("Could not convert price per unit in USD to a float64") + } + return instanceTypeName, pricePerUnitInUSD, nil + } + } + return instanceTypeName, float64(-1.0), fmt.Errorf("Unable to parse pricing doc") +} diff --git a/pkg/ec2pricing/ec2pricing_test.go b/pkg/ec2pricing/ec2pricing_test.go new file mode 100644 index 0000000..d507cb8 --- /dev/null +++ b/pkg/ec2pricing/ec2pricing_test.go @@ -0,0 +1,146 @@ +package ec2pricing_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "testing" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" + h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go/service/pricing" + "github.com/aws/aws-sdk-go/service/pricing/pricingiface" +) + +const ( + getProductsPages = "GetProductsPages" + describeSpotPriceHistoryPages = "DescribeSpotPriceHistoryPages" + mockFilesPath = "../../test/static" +) + +// Mocking helpers + +type gpFn = func(page *pricing.GetProductsOutput, lastPage bool) bool +type dspFn = func(page *ec2.DescribeSpotPriceHistoryOutput, lastPage bool) bool + +type mockedPricing struct { + pricingiface.PricingAPI + ec2iface.EC2API + GetProductsPagesResp pricing.GetProductsOutput + GetProductsPagesErr error + DescribeSpotPriceHistoryPagesResp ec2.DescribeSpotPriceHistoryOutput + DescribeSpotPriceHistoryPagesErr error +} + +func (m mockedPricing) GetProductsPages(input *pricing.GetProductsInput, fn gpFn) error { + fn(&m.GetProductsPagesResp, true) + return m.GetProductsPagesErr +} + +func (m mockedPricing) DescribeSpotPriceHistoryPages(input *ec2.DescribeSpotPriceHistoryInput, fn dspFn) error { + fn(&m.DescribeSpotPriceHistoryPagesResp, true) + return m.DescribeSpotPriceHistoryPagesErr +} + +func setupMock(t *testing.T, api string, file string) mockedPricing { + mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) + mockFile, err := ioutil.ReadFile(mockFilename) + h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) + switch api { + case getProductsPages: + var productsMap map[string]interface{} + err = json.Unmarshal(mockFile, &productsMap) + h.Assert(t, err == nil, "Error parsing mock json file contents "+mockFilename) + productsOutput := pricing.GetProductsOutput{ + PriceList: []aws.JSONValue{productsMap}, + } + return mockedPricing{ + GetProductsPagesResp: productsOutput, + } + case describeSpotPriceHistoryPages: + dspho := ec2.DescribeSpotPriceHistoryOutput{} + err = json.Unmarshal(mockFile, &dspho) + h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) + return mockedPricing{ + DescribeSpotPriceHistoryPagesResp: dspho, + } + + default: + h.Assert(t, false, "Unable to mock the provided API type "+api) + } + return mockedPricing{} +} + +func TestGetOndemandInstanceTypeCost_m5large(t *testing.T) { + sess := session.Session{ + Config: &aws.Config{ + Region: aws.String("us-east-1"), + }, + } + pricingMock := setupMock(t, getProductsPages, "m5_large.json") + ec2pricingClient := ec2pricing.EC2Pricing{ + PricingClient: pricingMock, + AWSSession: &sess, + } + price, err := ec2pricingClient.GetOndemandInstanceTypeCost("m5.large") + h.Ok(t, err) + h.Equals(t, float64(0.096), price) +} + +func TestHydrateOndemandCache(t *testing.T) { + sess := session.Session{ + Config: &aws.Config{ + Region: aws.String("us-east-1"), + }, + } + pricingMock := setupMock(t, getProductsPages, "m5_large.json") + ec2pricingClient := ec2pricing.EC2Pricing{ + PricingClient: pricingMock, + AWSSession: &sess, + } + err := ec2pricingClient.HydrateOndemandCache() + h.Ok(t, err) + + price, err := ec2pricingClient.GetOndemandInstanceTypeCost("m5.large") + h.Ok(t, err) + h.Equals(t, float64(0.096), price) +} + +func TestGetSpotInstanceTypeNDayAvgCost(t *testing.T) { + sess := session.Session{ + Config: &aws.Config{ + Region: aws.String("us-east-1"), + }, + } + ec2Mock := setupMock(t, describeSpotPriceHistoryPages, "m5_large.json") + ec2pricingClient := ec2pricing.EC2Pricing{ + EC2Client: ec2Mock, + AWSSession: &sess, + } + price, err := ec2pricingClient.GetSpotInstanceTypeNDayAvgCost("m5.large", []string{"us-east-1a"}, 30) + h.Ok(t, err) + h.Equals(t, float64(0.041486231229302666), price) +} + +func TestHydrateSpotCache(t *testing.T) { + sess := session.Session{ + Config: &aws.Config{ + Region: aws.String("us-east-1"), + }, + } + ec2Mock := setupMock(t, describeSpotPriceHistoryPages, "m5_large.json") + ec2pricingClient := ec2pricing.EC2Pricing{ + EC2Client: ec2Mock, + AWSSession: &sess, + } + err := ec2pricingClient.HydrateSpotCache(30) + h.Ok(t, err) + + price, err := ec2pricingClient.GetSpotInstanceTypeNDayAvgCost("m5.large", []string{"us-east-1a"}, 30) + h.Ok(t, err) + h.Equals(t, float64(0.041486231229302666), price) +} diff --git a/pkg/instancetypes/instancetypes.go b/pkg/instancetypes/instancetypes.go new file mode 100644 index 0000000..3fdc54c --- /dev/null +++ b/pkg/instancetypes/instancetypes.go @@ -0,0 +1,10 @@ +package instancetypes + +import "github.com/aws/aws-sdk-go/service/ec2" + +// Details hold all the information on an ec2 instance type +type Details struct { + ec2.InstanceTypeInfo + OndemandPricePerHour *float64 + SpotPrice *float64 +} diff --git a/pkg/selector/comparators.go b/pkg/selector/comparators.go index f39151d..193a2d5 100644 --- a/pkg/selector/comparators.go +++ b/pkg/selector/comparators.go @@ -91,6 +91,17 @@ func isSupportedWithRangeUint64(instanceTypeValue *int64, target *Uint64RangeFil return uint64(*instanceTypeValue) >= target.LowerBound && uint64(*instanceTypeValue) <= target.UpperBound } +func isSupportedWithRangeFloat64(instanceTypeValue *float64, target *Float64RangeFilter) bool { + if target == nil { + return true + } else if instanceTypeValue == nil && target.LowerBound == 0.0 && target.UpperBound == 0.0 { + return true + } else if instanceTypeValue == nil { + return false + } + return float64(*instanceTypeValue) >= target.LowerBound && float64(*instanceTypeValue) <= target.UpperBound +} + func isSupportedWithBool(instanceTypeValue *bool, target *bool) bool { if target == nil { return true diff --git a/pkg/selector/outputs/outputs.go b/pkg/selector/outputs/outputs.go index ac59832..39aeb05 100644 --- a/pkg/selector/outputs/outputs.go +++ b/pkg/selector/outputs/outputs.go @@ -19,15 +19,16 @@ import ( "encoding/json" "fmt" "log" + "strconv" "strings" "text/tabwriter" - "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/ghodss/yaml" ) // SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names -func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func SimpleInstanceTypeOutput(instanceTypeInfoSlice []instancetypes.Details) []string { instanceTypeStrings := []string{} for _, instanceTypeInfo := range instanceTypeInfoSlice { instanceTypeStrings = append(instanceTypeStrings, *instanceTypeInfo.InstanceType) @@ -36,7 +37,7 @@ func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []s } // VerboseInstanceTypeOutput is an OutputFn which outputs a slice of instance type names -func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func VerboseInstanceTypeOutput(instanceTypeInfoSlice []instancetypes.Details) []string { output, err := json.MarshalIndent(instanceTypeInfoSlice, "", " ") if err != nil { log.Println("Unable to convert instance type info to JSON") @@ -49,7 +50,7 @@ func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) [] } // TerraformSpotMixedInstancesPolicyHCLOutput is an OutputFn which returns an ASG MixedInstancePolicy in Terraform HCL syntax -func TerraformSpotMixedInstancesPolicyHCLOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func TerraformSpotMixedInstancesPolicyHCLOutput(instanceTypeInfoSlice []instancetypes.Details) []string { instanceTypeOverrides := instanceTypeInfoToOverrides(instanceTypeInfoSlice) overridesString := "" for _, override := range instanceTypeOverrides { @@ -95,7 +96,7 @@ func TerraformSpotMixedInstancesPolicyHCLOutput(instanceTypeInfoSlice []*ec2.Ins } // CloudFormationSpotMixedInstancesPolicyYAMLOutput is an OutputFn which returns an ASG MixedInstancePolicy in CloudFormation YAML syntax -func CloudFormationSpotMixedInstancesPolicyYAMLOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func CloudFormationSpotMixedInstancesPolicyYAMLOutput(instanceTypeInfoSlice []instancetypes.Details) []string { instanceTypeOverrides := instanceTypeInfoToOverrides(instanceTypeInfoSlice) cfnMig := getCfnMIGResources(instanceTypeOverrides) cfnMigYAML, err := yaml.Marshal(cfnMig) @@ -106,7 +107,7 @@ func CloudFormationSpotMixedInstancesPolicyYAMLOutput(instanceTypeInfoSlice []*e } // CloudFormationSpotMixedInstancesPolicyJSONOutput is an OutputFn which returns an MixedInstancePolicy in CloudFormation JSON syntax -func CloudFormationSpotMixedInstancesPolicyJSONOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func CloudFormationSpotMixedInstancesPolicyJSONOutput(instanceTypeInfoSlice []instancetypes.Details) []string { instanceTypeOverrides := instanceTypeInfoToOverrides(instanceTypeInfoSlice) cfnMig := getCfnMIGResources(instanceTypeOverrides) cfnJSONMig, err := json.MarshalIndent(cfnMig, "", " ") @@ -143,7 +144,7 @@ func getCfnMIGResources(instanceTypeOverrides []InstanceTypeOverride) Resources return Resources{Resources: resources} } -func instanceTypeInfoToOverrides(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []InstanceTypeOverride { +func instanceTypeInfoToOverrides(instanceTypeInfoSlice []instancetypes.Details) []InstanceTypeOverride { instanceTypeOverrides := []InstanceTypeOverride{} for _, instanceTypeInfo := range instanceTypeInfoSlice { instanceTypeOverrides = append(instanceTypeOverrides, InstanceTypeOverride{InstanceType: *instanceTypeInfo.InstanceType}) @@ -152,7 +153,7 @@ func instanceTypeInfoToOverrides(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) } // TableOutputShort is an OutputFn which returns a CLI table for easy reading -func TableOutputShort(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func TableOutputShort(instanceTypeInfoSlice []instancetypes.Details) []string { if instanceTypeInfoSlice == nil || len(instanceTypeInfoSlice) == 0 { return nil } @@ -177,10 +178,10 @@ func TableOutputShort(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { fmt.Fprintf(w, "\n"+headerFormat, separators...) for _, instanceTypeInfo := range instanceTypeInfoSlice { - fmt.Fprintf(w, "\n%s\t%d\t%.3f\t", + fmt.Fprintf(w, "\n%s\t%d\t%s\t", *instanceTypeInfo.InstanceType, *instanceTypeInfo.VCpuInfo.DefaultVCpus, - float64(*instanceTypeInfo.MemoryInfo.SizeInMiB)/1024.0, + formatFloat(float64(*instanceTypeInfo.MemoryInfo.SizeInMiB)/1024.0), ) } w.Flush() @@ -188,7 +189,7 @@ func TableOutputShort(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { } // TableOutputWide is an OutputFn which returns a detailed CLI table for easy reading -func TableOutputWide(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func TableOutputWide(instanceTypeInfoSlice []instancetypes.Details) []string { if instanceTypeInfoSlice == nil || len(instanceTypeInfoSlice) == 0 { return nil } @@ -198,6 +199,11 @@ func TableOutputWide(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { w.Init(buf, 8, 8, 2, ' ', 0) defer w.Flush() + pricePerHourHeader := "On-Demand Price/Hr" + if instanceTypeInfoSlice[0].SpotPrice != nil { + pricePerHourHeader = "Spot Price/Hr (30 days)" + } + headers := []interface{}{ "Instance Type", "VCPUs", @@ -211,6 +217,7 @@ func TableOutputWide(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { "GPUs", "GPU Mem (GiB)", "GPU Info", + pricePerHourHeader, } separators := []interface{}{} @@ -242,10 +249,20 @@ func TableOutputWide(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { } } - fmt.Fprintf(w, "\n%s\t%d\t%.3f\t%s\t%t\t%t\t%s\t%s\t%d\t%d\t%.2f\t%s\t", + pricePerHour := instanceTypeInfo.OndemandPricePerHour + if instanceTypeInfo.SpotPrice != nil { + pricePerHour = instanceTypeInfo.SpotPrice + } + specifyPriceFilter := "-No Price Filter Specified-" + pricePerHourStr := specifyPriceFilter + if pricePerHour != nil { + pricePerHourStr = fmt.Sprintf("$%s", formatFloat(*pricePerHour)) + } + + fmt.Fprintf(w, "\n%s\t%d\t%s\t%s\t%t\t%t\t%s\t%s\t%d\t%d\t%s\t%s\t%s\t", *instanceTypeInfo.InstanceType, *instanceTypeInfo.VCpuInfo.DefaultVCpus, - float64(*instanceTypeInfo.MemoryInfo.SizeInMiB)/1024.0, + formatFloat(float64(*instanceTypeInfo.MemoryInfo.SizeInMiB)/1024.0), *hypervisor, *instanceTypeInfo.CurrentGeneration, *instanceTypeInfo.HibernationSupported, @@ -253,8 +270,9 @@ func TableOutputWide(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { *instanceTypeInfo.NetworkInfo.NetworkPerformance, *instanceTypeInfo.NetworkInfo.MaximumNetworkInterfaces, gpus, - float64(gpuMemory)/1024.0, + formatFloat(float64(gpuMemory)/1024.0), strings.Join(gpuType, ", "), + pricePerHourStr, ) } w.Flush() @@ -262,7 +280,7 @@ func TableOutputWide(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { } // OneLineOutput is an output function which prints the instance type names on a single line separated by commas -func OneLineOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { +func OneLineOutput(instanceTypeInfoSlice []instancetypes.Details) []string { instanceTypeNames := []string{} for _, instanceType := range instanceTypeInfoSlice { instanceTypeNames = append(instanceTypeNames, *instanceType.InstanceType) @@ -272,3 +290,26 @@ func OneLineOutput(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []string { } return []string{strings.Join(instanceTypeNames, ",")} } + +func formatFloat(f float64) string { + s := strconv.FormatFloat(f, 'f', 5, 64) + parts := strings.Split(s, ".") + reversed := reverse(parts[0]) + withCommas := "" + for i, p := range reversed { + if i%3 == 0 && i != 0 { + withCommas += "," + } + withCommas += string(p) + } + s = strings.Join([]string{reverse(withCommas), parts[1]}, ".") + return strings.TrimRight(strings.TrimRight(s, "0"), ".") +} + +func reverse(s string) string { + runes := []rune(s) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index b8bf67c..c7ccafe 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" "github.com/aws/aws-sdk-go/service/ec2" @@ -32,14 +33,19 @@ const ( mockFilesPath = "../../../test/static" ) -func getInstanceTypes(t *testing.T, file string) []*ec2.InstanceTypeInfo { +func getInstanceTypes(t *testing.T, file string) []instancetypes.Details { mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, describeInstanceTypes, file) mockFile, err := ioutil.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) dito := ec2.DescribeInstanceTypesOutput{} err = json.Unmarshal(mockFile, &dito) h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) - return dito.InstanceTypes + instanceTypesDetails := []instancetypes.Details{} + for _, it := range dito.InstanceTypes { + odPrice := float64(0.53) + instanceTypesDetails = append(instanceTypesDetails, instancetypes.Details{InstanceTypeInfo: *it, OndemandPricePerHour: &odPrice}) + } + return instanceTypesDetails } func TestSimpleInstanceTypeOutput(t *testing.T) { @@ -48,7 +54,7 @@ func TestSimpleInstanceTypeOutput(t *testing.T) { h.Assert(t, len(instanceTypeOut) == len(instanceTypes), "Should return the same number of instance types as the data passed in") h.Assert(t, instanceTypeOut[0] == "t3.micro", "Should only return t3.micro") - instanceTypeOut = outputs.SimpleInstanceTypeOutput([]*ec2.InstanceTypeInfo{}) + instanceTypeOut = outputs.SimpleInstanceTypeOutput([]instancetypes.Details{}) h.Assert(t, len(instanceTypeOut) == 0, "Should return 0 instance types when passed empty slice") instanceTypeOut = outputs.SimpleInstanceTypeOutput(nil) @@ -64,7 +70,7 @@ func TestVerboseInstanceTypeOutput(t *testing.T) { h.Assert(t, len(instanceTypeOut) == len(instanceTypes), "Should return the same number of instance types as the data passed in") h.Assert(t, instanceTypeOut[0] == string(outputExpectation), "Should only return t3.micro") - instanceTypeOut = outputs.VerboseInstanceTypeOutput([]*ec2.InstanceTypeInfo{}) + instanceTypeOut = outputs.VerboseInstanceTypeOutput([]instancetypes.Details{}) h.Assert(t, len(instanceTypeOut) == 0, "Should return 0 instance types when passed empty slice") instanceTypeOut = outputs.VerboseInstanceTypeOutput(nil) @@ -122,12 +128,12 @@ func TestTableOutput_MBtoGB(t *testing.T) { instanceTypes := getInstanceTypes(t, "g2_2xlarge.json") instanceTypeOut := outputs.TableOutputWide(instanceTypes) outputStr := strings.Join(instanceTypeOut, "") - h.Assert(t, strings.Contains(outputStr, "15.000"), "table should include 15.000 GB of memory") - h.Assert(t, strings.Contains(outputStr, "4.00"), "wide table should include 4.00 GB of gpu memory") + h.Assert(t, strings.Contains(outputStr, "15"), "table should include 15 GB of memory") + h.Assert(t, strings.Contains(outputStr, "4"), "wide table should include 4 GB of gpu memory") instanceTypeOut = outputs.TableOutputShort(instanceTypes) outputStr = strings.Join(instanceTypeOut, "") - h.Assert(t, strings.Contains(outputStr, "15.000"), "table should include 15.000 GB of memory") + h.Assert(t, strings.Contains(outputStr, "15"), "table should include 15 GB of memory") } func TestOneLineOutput(t *testing.T) { @@ -136,7 +142,7 @@ func TestOneLineOutput(t *testing.T) { h.Assert(t, len(instanceTypeOut) == 1, "Should always return 1 line") h.Assert(t, instanceTypeOut[0] == "t3.micro,p3.16xlarge", "Should return both instance types separated by a comma") - instanceTypeOut = outputs.OneLineOutput([]*ec2.InstanceTypeInfo{}) + instanceTypeOut = outputs.OneLineOutput([]instancetypes.Details{}) h.Assert(t, len(instanceTypeOut) == 0, "Should return 0 instance types when passed empty slice") instanceTypeOut = outputs.OneLineOutput(nil) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 3f75d11..7db9473 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -21,6 +21,8 @@ import ( "sort" "strings" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" @@ -70,17 +72,20 @@ const ( virtualizationTypeParaVirtual = "paravirtual" virtualizationTypePV = "pv" + + pricePerHour = "pricePerHour" ) // New creates an instance of Selector provided an aws session func New(sess *session.Session) *Selector { serviceRegistry := NewRegistry() serviceRegistry.RegisterAWSServices() - userAgentTag := fmt.Sprintf("%s-v%s", sdkName, versionID) + userAgentTag := fmt.Sprintf("%s-%s", sdkName, versionID) userAgentHandler := request.MakeAddToUserAgentFreeFormHandler(userAgentTag) sess.Handlers.Build.PushBack(userAgentHandler) return &Selector{ EC2: ec2.New(sess), + EC2Pricing: ec2pricing.New(sess), ServiceRegistry: serviceRegistry, } } @@ -95,7 +100,7 @@ func (itf Selector) Filter(filters Filters) ([]string, error) { // FilterVerbose accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a list instanceTypeInfo -func (itf Selector) FilterVerbose(filters Filters) ([]*ec2.InstanceTypeInfo, error) { +func (itf Selector) FilterVerbose(filters Filters) ([]instancetypes.Details, error) { instanceTypeInfoSlice, err := itf.rawFilter(filters) if err != nil { return nil, err @@ -116,7 +121,7 @@ func (itf Selector) FilterWithOutput(filters Filters, outputFn InstanceTypesOutp return output, numOfItemsTruncated, nil } -func (itf Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []*ec2.InstanceTypeInfo) ([]*ec2.InstanceTypeInfo, int) { +func (itf Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []instancetypes.Details) ([]instancetypes.Details, int) { if maxResults == nil { return instanceTypeInfoSlice, 0 } @@ -146,7 +151,7 @@ func (itf Selector) AggregateFilterTransform(filters Filters) (Filters, error) { // rawFilter accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns the detailed specs of matching instance types -func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error) { +func (itf Selector) rawFilter(filters Filters) ([]instancetypes.Details, error) { filters, err := itf.AggregateFilterTransform(filters) if err != nil { return nil, err @@ -171,15 +176,35 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error) } instanceTypesInput := &ec2.DescribeInstanceTypesInput{} - instanceTypeCandidates := map[string]*ec2.InstanceTypeInfo{} + instanceTypeCandidates := map[string]*instancetypes.Details{} // innerErr will hold any error while processing DescribeInstanceTypes pages var innerErr error err = itf.EC2.DescribeInstanceTypesPages(instanceTypesInput, func(page *ec2.DescribeInstanceTypesOutput, lastPage bool) bool { for _, instanceTypeInfo := range page.InstanceTypes { instanceTypeName := *instanceTypeInfo.InstanceType - instanceTypeCandidates[instanceTypeName] = instanceTypeInfo + instanceTypeCandidates[instanceTypeName] = &instancetypes.Details{InstanceTypeInfo: *instanceTypeInfo} isFpga := instanceTypeInfo.FpgaInfo != nil + instanceTypeHourlyPrice := float64(0.0) + if filters.PricePerHour != nil { + if filters.UsageClass != nil && *filters.UsageClass == "spot" { + azs := []string{} + if filters.AvailabilityZones != nil { + azs = *filters.AvailabilityZones + } + instanceTypeHourlyPrice, err = itf.EC2Pricing.GetSpotInstanceTypeNDayAvgCost(instanceTypeName, azs, 30) + if err != nil { + fmt.Printf("Could not retrieve 30 day avg spot price for instance type %s\n", instanceTypeName) + } + instanceTypeCandidates[instanceTypeName].SpotPrice = &instanceTypeHourlyPrice + } else { + instanceTypeHourlyPrice, err = itf.EC2Pricing.GetOndemandInstanceTypeCost(instanceTypeName) + if err != nil { + fmt.Printf("Could not retrieve hourly price for instance type %s\n", instanceTypeName) + } + instanceTypeCandidates[instanceTypeName].OndemandPricePerHour = &instanceTypeHourlyPrice + } + } // filterToInstanceSpecMappingPairs is a map of filter name [key] to filter pair [value]. // A filter pair includes user input filter value and instance spec value retrieved from DescribeInstanceTypes @@ -204,6 +229,7 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error) networkPerformance: {filters.NetworkPerformance, getNetworkPerformance(instanceTypeInfo.NetworkInfo.NetworkPerformance)}, instanceTypes: {filters.InstanceTypes, instanceTypeInfo.InstanceType}, virtualizationType: {filters.VirtualizationType, instanceTypeInfo.SupportedVirtualizationTypes}, + pricePerHour: {filters.PricePerHour, &instanceTypeHourlyPrice}, } if isInDenyList(filters.DenyList, instanceTypeName) || !isInAllowList(filters.AllowList, instanceTypeName) { @@ -234,15 +260,15 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error) return nil, innerErr } - instanceTypeInfoSlice := []*ec2.InstanceTypeInfo{} + instanceTypeInfoSlice := []instancetypes.Details{} for _, instanceTypeInfo := range instanceTypeCandidates { - instanceTypeInfoSlice = append(instanceTypeInfoSlice, instanceTypeInfo) + instanceTypeInfoSlice = append(instanceTypeInfoSlice, *instanceTypeInfo) } return sortInstanceTypeInfo(instanceTypeInfoSlice), nil } // sortInstanceTypeInfo will sort based on instance type info alpha-numerically -func sortInstanceTypeInfo(instanceTypeInfoSlice []*ec2.InstanceTypeInfo) []*ec2.InstanceTypeInfo { +func sortInstanceTypeInfo(instanceTypeInfoSlice []instancetypes.Details) []instancetypes.Details { sort.Slice(instanceTypeInfoSlice, func(i, j int) bool { iInstanceInfo := instanceTypeInfoSlice[i] jInstanceInfo := instanceTypeInfoSlice[j] @@ -303,6 +329,15 @@ func (itf Selector) executeFilters(filterToInstanceSpecMapping map[string]filter default: return false, fmt.Errorf(invalidInstanceSpecTypeMsg) } + case *Float64RangeFilter: + switch iSpec := instanceSpec.(type) { + case *float64: + if !isSupportedWithRangeFloat64(iSpec, filter) { + return false, nil + } + default: + return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + } case *ByteQuantityRangeFilter: mibRange := Uint64RangeFilter{ LowerBound: filter.LowerBound.Quantity, diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index 785a20d..2ae5104 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -591,3 +591,106 @@ func TestFilter_VirtType_PV(t *testing.T) { h.Ok(t, err) h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") } + +type ec2PricingMock struct { + GetOndemandInstanceTypeCostResp float64 + GetOndemandInstanceTypeCostErr error + GetSpotInstanceTypeNDayAvgCostResp float64 + GetSpotInstanceTypeNDayAvgCostErr error + HydrateOndemandCacheErr error + HydrateSpotCacheErr error +} + +func (p *ec2PricingMock) GetOndemandInstanceTypeCost(instanceType string) (float64, error) { + return p.GetOndemandInstanceTypeCostResp, p.GetOndemandInstanceTypeCostErr +} + +func (p *ec2PricingMock) GetSpotInstanceTypeNDayAvgCost(instanceType string, availabilityZones []string, days int) (float64, error) { + return p.GetSpotInstanceTypeNDayAvgCostResp, p.GetSpotInstanceTypeNDayAvgCostErr +} + +func (p *ec2PricingMock) HydrateOndemandCache() error { + return p.HydrateOndemandCacheErr +} + +func (p *ec2PricingMock) HydrateSpotCache(days int) error { + return p.HydrateSpotCacheErr +} + +func TestFilter_PricePerHour(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypesPages, "t3_micro.json") + itf := selector.Selector{ + EC2: ec2Mock, + EC2Pricing: &ec2PricingMock{ + GetOndemandInstanceTypeCostResp: 0.0104, + }, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0104, + UpperBound: 0.0104, + }, + } + results, err := itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should return 1 instance type") +} + +func TestFilter_PricePerHour_NoResults(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypesPages, "t3_micro.json") + itf := selector.Selector{ + EC2: ec2Mock, + EC2Pricing: &ec2PricingMock{ + GetOndemandInstanceTypeCostResp: 0.0104, + }, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0105, + UpperBound: 0.0105, + }, + } + results, err := itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 0, "Should return 0 instance types") +} + +func TestFilter_PricePerHour_OD(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypesPages, "t3_micro.json") + itf := selector.Selector{ + EC2: ec2Mock, + EC2Pricing: &ec2PricingMock{ + GetOndemandInstanceTypeCostResp: 0.0104, + }, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0104, + UpperBound: 0.0104, + }, + UsageClass: aws.String("on-demand"), + } + results, err := itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should return 1 instance type") +} + +func TestFilter_PricePerHour_Spot(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypesPages, "t3_micro.json") + itf := selector.Selector{ + EC2: ec2Mock, + EC2Pricing: &ec2PricingMock{ + GetSpotInstanceTypeNDayAvgCostResp: 0.0104, + }, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0104, + UpperBound: 0.0104, + }, + UsageClass: aws.String("spot"), + } + results, err := itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should return 1 instance type") +} diff --git a/pkg/selector/types.go b/pkg/selector/types.go index 835e461..da4a9f6 100644 --- a/pkg/selector/types.go +++ b/pkg/selector/types.go @@ -18,27 +18,29 @@ import ( "regexp" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" - "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" ) // InstanceTypesOutput can be implemented to provide custom output to instance type results type InstanceTypesOutput interface { - Output([]*ec2.InstanceTypeInfo) []string + Output([]instancetypes.Details) []string } // InstanceTypesOutputFn is the func type definition for InstanceTypesOuput -type InstanceTypesOutputFn func([]*ec2.InstanceTypeInfo) []string +type InstanceTypesOutputFn func([]instancetypes.Details) []string // Output implements InstanceTypesOutput interface on InstanceTypesOutputFn // This allows any InstanceTypesOutputFn to be passed into funcs accepting InstanceTypesOutput interface -func (fn InstanceTypesOutputFn) Output(instanceTypes []*ec2.InstanceTypeInfo) []string { +func (fn InstanceTypesOutputFn) Output(instanceTypes []instancetypes.Details) []string { return fn(instanceTypes) } // Selector is used to filter instance type resource specs type Selector struct { EC2 ec2iface.EC2API + EC2Pricing ec2pricing.EC2PricingIface ServiceRegistry ServiceRegistry } @@ -63,6 +65,13 @@ type ByteQuantityRangeFilter struct { LowerBound bytequantity.ByteQuantity } +// Float64RangeFilter holds an upper and lower bound float64 +// The lower and upper bound are used to range filter resource specs +type Float64RangeFilter struct { + UpperBound float64 + LowerBound float64 +} + // filterPair holds a tuple of the passed in filter value and the instance resource spec value type filterPair struct { filterValue interface{} @@ -190,4 +199,7 @@ type Filters struct { // VirtualizationType is used to return instance types that match either hvm or pv virtualization types VirtualizationType *string + + // PricePerHour is used to return instance types that are equal to or cheaper than the specified price + PricePerHour *Float64RangeFilter } diff --git a/test/static/DescribeSpotPriceHistoryPages/m5_large.json b/test/static/DescribeSpotPriceHistoryPages/m5_large.json new file mode 100644 index 0000000..342ba0d --- /dev/null +++ b/test/static/DescribeSpotPriceHistoryPages/m5_large.json @@ -0,0 +1,1754 @@ +{ + "SpotPriceHistory": [ + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043700", + "Timestamp": "2021-02-09T01:40:10+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042300", + "Timestamp": "2021-02-08T23:58:38+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038900", + "Timestamp": "2021-02-08T21:46:09+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-02-08T20:38:39+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043500", + "Timestamp": "2021-02-08T19:30:39+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-02-08T19:14:31+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043100", + "Timestamp": "2021-02-08T19:14:31+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038800", + "Timestamp": "2021-02-08T16:56:08+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043400", + "Timestamp": "2021-02-08T13:20:14+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042200", + "Timestamp": "2021-02-08T00:28:09+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-02-07T19:14:30+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043500", + "Timestamp": "2021-02-07T19:14:30+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038700", + "Timestamp": "2021-02-07T19:14:30+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042100", + "Timestamp": "2021-02-07T19:14:30+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043100", + "Timestamp": "2021-02-07T19:14:30+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043100", + "Timestamp": "2021-02-07T15:01:46+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042100", + "Timestamp": "2021-02-07T06:50:17+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-02-07T04:09:17+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043500", + "Timestamp": "2021-02-07T00:37:17+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038700", + "Timestamp": "2021-02-07T00:11:46+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043200", + "Timestamp": "2021-02-06T23:04:20+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043100", + "Timestamp": "2021-02-06T15:01:36+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-02-06T14:44:42+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042100", + "Timestamp": "2021-02-06T06:50:16+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038600", + "Timestamp": "2021-02-06T05:59:18+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043200", + "Timestamp": "2021-02-05T23:04:19+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038500", + "Timestamp": "2021-02-05T22:47:47+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042000", + "Timestamp": "2021-02-05T15:01:34+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043100", + "Timestamp": "2021-02-05T15:01:34+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-02-05T14:44:41+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043000", + "Timestamp": "2021-02-05T09:48:40+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-02-05T02:36:41+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038400", + "Timestamp": "2021-02-05T02:11:12+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-02-04T21:22:56+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-02-04T21:22:56+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-02-04T19:07:33+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-02-04T16:01:32+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042000", + "Timestamp": "2021-02-04T15:01:33+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043100", + "Timestamp": "2021-02-04T15:01:33+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042600", + "Timestamp": "2021-02-04T09:48:01+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042500", + "Timestamp": "2021-02-04T03:18:32+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.043000", + "Timestamp": "2021-02-03T23:13:04+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042400", + "Timestamp": "2021-02-03T21:48:31+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041900", + "Timestamp": "2021-02-03T21:23:01+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-02-03T19:07:32+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-02-03T17:22:58+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-02-03T16:01:32+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042300", + "Timestamp": "2021-02-03T15:44:33+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041800", + "Timestamp": "2021-02-03T07:42:04+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-02-03T04:53:07+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042200", + "Timestamp": "2021-02-03T03:11:32+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-02-03T00:56:04+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-02-02T22:14:01+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041700", + "Timestamp": "2021-02-02T18:04:31+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-02-02T17:22:57+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042100", + "Timestamp": "2021-02-02T16:06:02+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-02-02T15:40:56+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042000", + "Timestamp": "2021-02-02T09:36:34+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-02-02T07:04:06+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041800", + "Timestamp": "2021-02-02T02:08:01+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-02-02T01:00:02+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041700", + "Timestamp": "2021-02-01T19:38:27+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-02-01T18:24:26+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-02-01T17:22:57+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-02-01T15:40:55+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-02-01T14:26:01+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-02-01T13:18:38+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-02-01T11:43:57+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-02-01T01:43:26+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-31T22:39:27+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-31T18:24:26+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-31T16:18:49+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-31T14:26:00+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-31T13:18:36+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-01-31T00:38:07+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-30T22:39:25+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-30T18:24:25+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-30T16:18:43+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041700", + "Timestamp": "2021-01-30T16:17:25+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-30T13:11:25+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041800", + "Timestamp": "2021-01-30T10:04:54+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-30T06:24:54+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041900", + "Timestamp": "2021-01-30T04:51:55+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-01-30T00:38:07+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-29T22:39:25+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-01-29T21:57:32+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-29T19:33:33+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041800", + "Timestamp": "2021-01-29T18:59:27+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-29T16:18:32+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-29T15:02:35+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041700", + "Timestamp": "2021-01-29T01:20:58+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-01-28T21:57:26+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-28T19:33:31+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-28T16:18:30+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-28T15:02:34+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-28T11:36:06+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-28T11:13:40+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-28T09:29:09+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-28T02:42:40+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-28T01:54:35+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-28T01:43:44+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-27T11:36:06+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-27T09:29:06+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-27T02:42:39+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-27T02:25:07+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-27T01:43:42+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-26T13:17:37+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-26T13:09:06+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-26T11:36:05+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-26T06:06:07+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042900", + "Timestamp": "2021-01-26T04:32:35+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-26T02:42:36+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-26T01:43:35+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041700", + "Timestamp": "2021-01-26T01:01:06+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-25T23:02:37+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-25T19:47:35+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-25T19:46:32+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-25T19:39:47+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041100", + "Timestamp": "2021-01-25T15:42:09+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-25T12:52:35+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041200", + "Timestamp": "2021-01-25T09:12:42+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-25T08:54:34+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041100", + "Timestamp": "2021-01-24T22:29:24+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-24T19:46:31+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-24T19:39:43+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041000", + "Timestamp": "2021-01-24T17:32:06+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-24T15:50:38+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-24T14:59:04+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-24T08:54:33+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041100", + "Timestamp": "2021-01-24T05:06:03+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-24T03:24:03+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-23T19:46:31+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-23T14:59:00+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-23T08:54:30+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041100", + "Timestamp": "2021-01-23T05:06:03+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038400", + "Timestamp": "2021-01-23T03:23:59+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-23T03:23:59+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041700", + "Timestamp": "2021-01-23T01:34:01+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041000", + "Timestamp": "2021-01-22T23:43:59+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-22T20:54:31+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-22T19:53:54+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-22T19:46:30+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040900", + "Timestamp": "2021-01-22T10:44:00+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041600", + "Timestamp": "2021-01-22T04:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-22T01:33:19+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-22T01:22:59+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040800", + "Timestamp": "2021-01-21T22:09:24+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-21T20:58:36+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-21T19:53:54+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-21T19:45:24+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-01-21T15:05:17+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-21T13:47:46+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040700", + "Timestamp": "2021-01-21T10:08:47+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-21T03:04:48+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-21T01:22:50+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040600", + "Timestamp": "2021-01-20T21:51:28+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-20T20:58:35+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-20T20:00:47+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-20T13:47:46+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-01-20T12:40:18+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-20T10:41:16+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041500", + "Timestamp": "2021-01-20T06:10:19+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-20T05:47:12+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040300", + "Timestamp": "2021-01-19T22:15:29+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-01-19T21:58:00+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-19T20:58:32+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-19T17:19:03+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-19T05:47:11+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040200", + "Timestamp": "2021-01-19T03:20:00+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041300", + "Timestamp": "2021-01-18T23:30:58+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-01-18T21:57:59+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-18T20:58:31+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-18T15:48:56+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-18T14:23:55+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-18T11:00:28+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040300", + "Timestamp": "2021-01-18T08:28:13+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037900", + "Timestamp": "2021-01-18T05:47:10+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040400", + "Timestamp": "2021-01-18T04:14:25+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-01-17T23:25:57+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038000", + "Timestamp": "2021-01-17T17:38:54+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-17T15:48:55+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-17T15:40:26+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-17T14:23:54+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040400", + "Timestamp": "2021-01-17T04:14:23+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-01-16T23:25:54+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-16T22:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041400", + "Timestamp": "2021-01-16T15:48:53+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038100", + "Timestamp": "2021-01-16T15:40:24+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042800", + "Timestamp": "2021-01-16T14:23:53+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037800", + "Timestamp": "2021-01-15T23:25:53+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-15T22:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-15T22:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037700", + "Timestamp": "2021-01-15T22:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041300", + "Timestamp": "2021-01-15T22:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-15T22:31:18+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-15T21:09:57+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041300", + "Timestamp": "2021-01-15T16:22:22+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038200", + "Timestamp": "2021-01-15T15:48:21+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-15T14:15:22+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040400", + "Timestamp": "2021-01-15T04:13:24+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037700", + "Timestamp": "2021-01-15T03:31:25+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-14T21:09:56+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038300", + "Timestamp": "2021-01-14T20:53:22+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037600", + "Timestamp": "2021-01-14T20:52:55+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041200", + "Timestamp": "2021-01-14T20:01:55+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038400", + "Timestamp": "2021-01-14T15:22:22+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040400", + "Timestamp": "2021-01-14T04:13:21+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038500", + "Timestamp": "2021-01-14T03:05:21+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-14T02:39:17+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042700", + "Timestamp": "2021-01-13T21:09:53+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037600", + "Timestamp": "2021-01-13T20:52:54+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041200", + "Timestamp": "2021-01-13T20:01:51+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038600", + "Timestamp": "2021-01-13T15:56:22+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041100", + "Timestamp": "2021-01-13T15:05:09+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042600", + "Timestamp": "2021-01-13T10:08:26+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037500", + "Timestamp": "2021-01-13T07:27:51+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-13T02:39:12+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038700", + "Timestamp": "2021-01-12T22:08:22+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041100", + "Timestamp": "2021-01-12T15:05:03+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037400", + "Timestamp": "2021-01-12T14:13:52+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042600", + "Timestamp": "2021-01-12T10:08:23+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038800", + "Timestamp": "2021-01-12T09:25:53+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037300", + "Timestamp": "2021-01-12T07:01:52+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-12T02:39:08+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042500", + "Timestamp": "2021-01-12T01:06:27+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.041000", + "Timestamp": "2021-01-11T19:44:07+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038900", + "Timestamp": "2021-01-11T19:18:43+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037200", + "Timestamp": "2021-01-11T18:28:06+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040900", + "Timestamp": "2021-01-11T18:28:04+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037100", + "Timestamp": "2021-01-11T02:56:04+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040500", + "Timestamp": "2021-01-11T02:39:04+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042500", + "Timestamp": "2021-01-11T01:06:26+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.038900", + "Timestamp": "2021-01-10T19:18:36+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.039000", + "Timestamp": "2021-01-10T19:10:25+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040900", + "Timestamp": "2021-01-10T18:28:04+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.037000", + "Timestamp": "2021-01-10T12:32:05+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040600", + "Timestamp": "2021-01-10T05:20:05+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040800", + "Timestamp": "2021-01-10T03:56:01+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042500", + "Timestamp": "2021-01-10T01:06:25+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.039000", + "Timestamp": "2021-01-09T19:10:25+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.036900", + "Timestamp": "2021-01-09T18:45:06+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040700", + "Timestamp": "2021-01-09T17:03:23+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040800", + "Timestamp": "2021-01-09T03:55:58+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.036800", + "Timestamp": "2021-01-09T01:06:25+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042500", + "Timestamp": "2021-01-09T01:06:25+00:00" + }, + { + "AvailabilityZone": "us-east-1a", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040700", + "Timestamp": "2021-01-08T21:51:23+00:00" + }, + { + "AvailabilityZone": "us-east-1b", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.036700", + "Timestamp": "2021-01-08T19:35:54+00:00" + }, + { + "AvailabilityZone": "us-east-1c", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.039000", + "Timestamp": "2021-01-08T19:10:25+00:00" + }, + { + "AvailabilityZone": "us-east-1f", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.040700", + "Timestamp": "2021-01-08T17:03:23+00:00" + }, + { + "AvailabilityZone": "us-east-1d", + "InstanceType": "m5.large", + "ProductDescription": "Linux/UNIX", + "SpotPrice": "0.042500", + "Timestamp": "2021-01-08T01:06:24+00:00" + } + ] +} diff --git a/test/static/GetProductsPages/m5_large.json b/test/static/GetProductsPages/m5_large.json new file mode 100644 index 0000000..750986d --- /dev/null +++ b/test/static/GetProductsPages/m5_large.json @@ -0,0 +1,413 @@ +{ + "product": { + "productFamily": "Compute Instance", + "attributes": { + "enhancedNetworkingSupported": "Yes", + "intelTurboAvailable": "Yes", + "memory": "8 GiB", + "dedicatedEbsThroughput": "Up to 2120 Mbps", + "vcpu": "2", + "capacitystatus": "Used", + "locationType": "AWS Region", + "storage": "EBS only", + "instanceFamily": "General purpose", + "operatingSystem": "Linux", + "intelAvx2Available": "Yes", + "physicalProcessor": "Intel Xeon Platinum 8175 (Skylake)", + "clockSpeed": "3.1 GHz", + "ecu": "10", + "networkPerformance": "Up to 10 Gigabit", + "servicename": "Amazon Elastic Compute Cloud", + "instanceType": "m5.large", + "tenancy": "Shared", + "usagetype": "BoxUsage:m5.large", + "normalizationSizeFactor": "4", + "intelAvxAvailable": "Yes", + "processorFeatures": "Intel AVX; Intel AVX2; Intel AVX512; Intel Turbo", + "servicecode": "AmazonEC2", + "licenseModel": "No License required", + "currentGeneration": "Yes", + "preInstalledSw": "NA", + "location": "US East (N. Virginia)", + "processorArchitecture": "64-bit", + "operation": "RunInstances" + }, + "sku": "6C86BEPQVG73ZGGR" + }, + "serviceCode": "AmazonEC2", + "terms": { + "OnDemand": { + "6C86BEPQVG73ZGGR.JRTCKXETXF": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.JRTCKXETXF.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "$0.096 per On Demand Linux m5.large Instance Hour", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.JRTCKXETXF.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0960000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2021-02-01T00:00:00Z", + "offerTermCode": "JRTCKXETXF", + "termAttributes": {} + } + }, + "Reserved": { + "6C86BEPQVG73ZGGR.4NA7Y494T4": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.4NA7Y494T4.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.4NA7Y494T4.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0600000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2020-04-01T00:00:00Z", + "offerTermCode": "4NA7Y494T4", + "termAttributes": { + "LeaseContractLength": "1yr", + "OfferingClass": "standard", + "PurchaseOption": "No Upfront" + } + }, + "6C86BEPQVG73ZGGR.CUZHX8X6JH": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.CUZHX8X6JH.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.CUZHX8X6JH.2TG2D8R56U", + "pricePerUnit": { + "USD": "294" + } + }, + "6C86BEPQVG73ZGGR.CUZHX8X6JH.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.CUZHX8X6JH.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0340000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2017-10-31T23:59:59Z", + "offerTermCode": "CUZHX8X6JH", + "termAttributes": { + "LeaseContractLength": "1yr", + "OfferingClass": "convertible", + "PurchaseOption": "Partial Upfront" + } + }, + "6C86BEPQVG73ZGGR.7NE97W5U4E": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.7NE97W5U4E.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.7NE97W5U4E.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0710000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2017-10-31T23:59:59Z", + "offerTermCode": "7NE97W5U4E", + "termAttributes": { + "LeaseContractLength": "1yr", + "OfferingClass": "convertible", + "PurchaseOption": "No Upfront" + } + }, + "6C86BEPQVG73ZGGR.38NPMPTW36": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.38NPMPTW36.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.38NPMPTW36.2TG2D8R56U", + "pricePerUnit": { + "USD": "505" + } + }, + "6C86BEPQVG73ZGGR.38NPMPTW36.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.38NPMPTW36.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0190000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2020-04-01T00:00:00Z", + "offerTermCode": "38NPMPTW36", + "termAttributes": { + "LeaseContractLength": "3yr", + "OfferingClass": "standard", + "PurchaseOption": "Partial Upfront" + } + }, + "6C86BEPQVG73ZGGR.R5XV2EPZQZ": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.R5XV2EPZQZ.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.R5XV2EPZQZ.2TG2D8R56U", + "pricePerUnit": { + "USD": "592" + } + }, + "6C86BEPQVG73ZGGR.R5XV2EPZQZ.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.R5XV2EPZQZ.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0230000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2017-10-31T23:59:59Z", + "offerTermCode": "R5XV2EPZQZ", + "termAttributes": { + "LeaseContractLength": "3yr", + "OfferingClass": "convertible", + "PurchaseOption": "Partial Upfront" + } + }, + "6C86BEPQVG73ZGGR.6QCMYABX3D": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.6QCMYABX3D.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.6QCMYABX3D.2TG2D8R56U", + "pricePerUnit": { + "USD": "494" + } + }, + "6C86BEPQVG73ZGGR.6QCMYABX3D.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "USD 0.0 per Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.6QCMYABX3D.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0000000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2020-04-01T00:00:00Z", + "offerTermCode": "6QCMYABX3D", + "termAttributes": { + "LeaseContractLength": "1yr", + "OfferingClass": "standard", + "PurchaseOption": "All Upfront" + } + }, + "6C86BEPQVG73ZGGR.NQ3QZPMQV9": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.NQ3QZPMQV9.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.NQ3QZPMQV9.2TG2D8R56U", + "pricePerUnit": { + "USD": "949" + } + }, + "6C86BEPQVG73ZGGR.NQ3QZPMQV9.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "USD 0.0 per Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.NQ3QZPMQV9.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0000000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2020-04-01T00:00:00Z", + "offerTermCode": "NQ3QZPMQV9", + "termAttributes": { + "LeaseContractLength": "3yr", + "OfferingClass": "standard", + "PurchaseOption": "All Upfront" + } + }, + "6C86BEPQVG73ZGGR.Z2E3P23VKM": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.Z2E3P23VKM.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.Z2E3P23VKM.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0490000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2017-10-31T23:59:59Z", + "offerTermCode": "Z2E3P23VKM", + "termAttributes": { + "LeaseContractLength": "3yr", + "OfferingClass": "convertible", + "PurchaseOption": "No Upfront" + } + }, + "6C86BEPQVG73ZGGR.MZU6U2429S": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.MZU6U2429S.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.MZU6U2429S.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0000000000" + } + }, + "6C86BEPQVG73ZGGR.MZU6U2429S.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.MZU6U2429S.2TG2D8R56U", + "pricePerUnit": { + "USD": "1161" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2017-10-31T23:59:59Z", + "offerTermCode": "MZU6U2429S", + "termAttributes": { + "LeaseContractLength": "3yr", + "OfferingClass": "convertible", + "PurchaseOption": "All Upfront" + } + }, + "6C86BEPQVG73ZGGR.BPH4J8HBKS": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.BPH4J8HBKS.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.BPH4J8HBKS.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0410000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2020-04-01T00:00:00Z", + "offerTermCode": "BPH4J8HBKS", + "termAttributes": { + "LeaseContractLength": "3yr", + "OfferingClass": "standard", + "PurchaseOption": "No Upfront" + } + }, + "6C86BEPQVG73ZGGR.HU7G6KETJZ": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.HU7G6KETJZ.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.HU7G6KETJZ.2TG2D8R56U", + "pricePerUnit": { + "USD": "252" + } + }, + "6C86BEPQVG73ZGGR.HU7G6KETJZ.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.HU7G6KETJZ.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0290000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2020-04-01T00:00:00Z", + "offerTermCode": "HU7G6KETJZ", + "termAttributes": { + "LeaseContractLength": "1yr", + "OfferingClass": "standard", + "PurchaseOption": "Partial Upfront" + } + }, + "6C86BEPQVG73ZGGR.VJWZNREJX2": { + "priceDimensions": { + "6C86BEPQVG73ZGGR.VJWZNREJX2.2TG2D8R56U": { + "unit": "Quantity", + "description": "Upfront Fee", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.VJWZNREJX2.2TG2D8R56U", + "pricePerUnit": { + "USD": "577" + } + }, + "6C86BEPQVG73ZGGR.VJWZNREJX2.6YS6EN2CT7": { + "unit": "Hrs", + "endRange": "Inf", + "description": "Linux/UNIX (Amazon VPC), m5.large reserved instance applied", + "appliesTo": [], + "rateCode": "6C86BEPQVG73ZGGR.VJWZNREJX2.6YS6EN2CT7", + "beginRange": "0", + "pricePerUnit": { + "USD": "0.0000000000" + } + } + }, + "sku": "6C86BEPQVG73ZGGR", + "effectiveDate": "2017-10-31T23:59:59Z", + "offerTermCode": "VJWZNREJX2", + "termAttributes": { + "LeaseContractLength": "1yr", + "OfferingClass": "convertible", + "PurchaseOption": "All Upfront" + } + } + } + }, + "version": "20210205204500", + "publicationDate": "2021-02-05T20:45:00Z" +}