open-nomad/tools/ec2info/main.go
Seth Hoenig ec0d5e9950 build: make ec2info command usable from GNUMakefile
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.
2022-09-09 08:48:12 -05:00

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: &region,
})
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
}