ec0d5e9950
The ec2info was never intuitive to run - needing to set the AWS envinronment variables, cd'ing into tools/ec2info, and knowing to invoke the command. This PR makes it so we can run ec2info just by running make ec2info The command now also checks for the AWS environment variables being set, and provides a useful error if they are not.
211 lines
5 KiB
Go
211 lines
5 KiB
Go
// Command ec2info provides a tool for generating a CPU performance lookup
|
|
// table indexed by EC2 instance types.
|
|
//
|
|
// By default the generated file will overwrite `env_aws_cpu.go` in Nomad's
|
|
// client/fingerprint package, when run from this directory.
|
|
//
|
|
// Requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN.
|
|
//
|
|
// Usage (invoke from Nomad's makefile)
|
|
//
|
|
// make ec2info
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"text/template"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
|
)
|
|
|
|
func check(err error) {
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
pkg, region, output := "fingerprint", "us-west-1", "client/fingerprint/env_aws_cpu.go"
|
|
|
|
client, err := clientForRegion(region)
|
|
check(err)
|
|
|
|
regions, err := getRegions(client)
|
|
check(err)
|
|
|
|
data, err := getData(regions)
|
|
check(err)
|
|
|
|
flat := flatten(data)
|
|
|
|
f, err := open(output)
|
|
check(err)
|
|
defer func() {
|
|
check(f.Close())
|
|
}()
|
|
|
|
check(write(f, flat, pkg))
|
|
check(format(output))
|
|
}
|
|
|
|
func clientForRegion(region string) (*ec2.EC2, error) {
|
|
sess, err := session.NewSession(&aws.Config{
|
|
Region: ®ion,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ec2.New(sess), nil
|
|
}
|
|
|
|
func getRegions(client *ec2.EC2) ([]*ec2.Region, error) {
|
|
all := false // beyond account access
|
|
regions, err := client.DescribeRegions(&ec2.DescribeRegionsInput{
|
|
AllRegions: &all,
|
|
})
|
|
if err != nil {
|
|
log.Println("failed to create AWS session; make sure environment is setup")
|
|
log.Println("must have environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN")
|
|
log.Println("or ~/.aws/credentials configured properly")
|
|
return nil, err
|
|
}
|
|
return regions.Regions, nil
|
|
}
|
|
|
|
type specs struct {
|
|
Cores int
|
|
Speed float64
|
|
}
|
|
|
|
func (s specs) String() string {
|
|
return fmt.Sprintf("(%d %.2f)", s.Cores, s.Speed)
|
|
}
|
|
|
|
func getData(regions []*ec2.Region) (map[string]map[string]specs, error) {
|
|
data := make(map[string]map[string]specs)
|
|
|
|
for _, region := range regions {
|
|
rData, rProblems, err := getDataForRegion(*region.RegionName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data[*region.RegionName] = rData
|
|
|
|
log.Println("region", *region.RegionName, "got data for", len(rData), "instance types", len(rProblems), "incomplete")
|
|
instanceProblems(rProblems)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func instanceProblems(problems map[string]string) {
|
|
types := make([]string, 0, len(problems))
|
|
for k := range problems {
|
|
types = append(types, k)
|
|
}
|
|
sort.Strings(types)
|
|
for _, iType := range types {
|
|
log.Println(" ->", iType, problems[iType])
|
|
}
|
|
}
|
|
|
|
func getDataForRegion(region string) (map[string]specs, map[string]string, error) {
|
|
client, err := clientForRegion(region)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
data := make(map[string]specs)
|
|
problems := make(map[string]string)
|
|
regionInfoPage(client, true, region, nil, data, problems)
|
|
return data, problems, nil
|
|
}
|
|
|
|
func regionInfoPage(client *ec2.EC2, first bool, region string, token *string, data map[string]specs, problems map[string]string) {
|
|
if first || token != nil {
|
|
output, err := client.DescribeInstanceTypes(&ec2.DescribeInstanceTypesInput{
|
|
NextToken: token,
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// recursively accumulate each page of data
|
|
regionInfoAccumulate(output, data, problems)
|
|
regionInfoPage(client, false, region, output.NextToken, data, problems)
|
|
}
|
|
}
|
|
|
|
func regionInfoAccumulate(output *ec2.DescribeInstanceTypesOutput, data map[string]specs, problems map[string]string) {
|
|
for _, iType := range output.InstanceTypes {
|
|
switch {
|
|
|
|
case iType.ProcessorInfo == nil:
|
|
fallthrough
|
|
case iType.ProcessorInfo.SustainedClockSpeedInGhz == nil:
|
|
problems[*iType.InstanceType] = "missing clock Speed"
|
|
continue
|
|
|
|
case iType.VCpuInfo == nil:
|
|
fallthrough
|
|
case iType.VCpuInfo.DefaultVCpus == nil:
|
|
problems[*iType.InstanceType] = "missing virtual cpu Cores"
|
|
continue
|
|
|
|
default:
|
|
data[*iType.InstanceType] = specs{
|
|
Speed: *iType.ProcessorInfo.SustainedClockSpeedInGhz,
|
|
Cores: int(*iType.VCpuInfo.DefaultVCpus),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// open the output file for writing.
|
|
func open(output string) (io.ReadWriteCloser, error) {
|
|
return os.Create(output)
|
|
}
|
|
|
|
// flatten region data, assuming instance type is the same across regions.
|
|
func flatten(data map[string]map[string]specs) map[string]specs {
|
|
result := make(map[string]specs)
|
|
for _, m := range data {
|
|
for iType, specifications := range m {
|
|
result[iType] = specifications
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type Template struct {
|
|
Package string
|
|
Data map[string]specs
|
|
}
|
|
|
|
// write the data using the cpu_table.go.template to w.
|
|
func write(w io.Writer, data map[string]specs, pkg string) error {
|
|
tmpl, err := template.ParseFiles("tools/ec2info/cpu_table.go.template")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tmpl.Execute(w, Template{
|
|
Package: pkg,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// format the file using gofmt.
|
|
func format(file string) error {
|
|
cmd := exec.Command("gofmt", "-w", file)
|
|
_, err := cmd.CombinedOutput()
|
|
return err
|
|
}
|