Skip to content

Commit

Permalink
Fetch pricing for ondemand instances and spot w/ cache implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bwagner5 committed Mar 1, 2021
1 parent fd56d04 commit 0f9c9a4
Show file tree
Hide file tree
Showing 19 changed files with 3,074 additions and 54 deletions.
53 changes: 36 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,50 +79,64 @@ $ 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**
```
$ 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**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const (
allowList = "allow-list"
denyList = "deny-list"
virtualizationType = "virtualization-type"
pricePerHour = "price-per-hour"
)

// Aggregate Filter Flags
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]),
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cli

import (
"fmt"
"math"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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
Expand Down
49 changes: 48 additions & 1 deletion pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
Expand Down
39 changes: 39 additions & 0 deletions pkg/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions pkg/cli/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
17 changes: 17 additions & 0 deletions pkg/cli/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions pkg/cli/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Loading

0 comments on commit 0f9c9a4

Please sign in to comment.