Skip to content

Commit

Permalink
Merge pull request #6779 from hashicorp/r-aws-fingerprint-via-library
Browse files Browse the repository at this point in the history
Use AWS SDK to access EC2 Metadata
  • Loading branch information
Mahmood Ali committed Dec 2, 2019
2 parents 830c0b1 + b55bc64 commit 10c97bf
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 333 deletions.
145 changes: 46 additions & 99 deletions client/fingerprint/env_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@ package fingerprint

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
log "github.com/hashicorp/go-hclog"

cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/nomad/nomad/structs"
)

const (
// This is where the AWS metadata server normally resides. We hardcode the
// "instance" path as well since it's the only one we access here.
DEFAULT_AWS_URL = "http://169.254.169.254/latest/meta-data/"

// AwsMetadataTimeout is the timeout used when contacting the AWS metadata
// service
AwsMetadataTimeout = 2 * time.Second
Expand Down Expand Up @@ -50,28 +49,35 @@ var ec2InstanceSpeedMap = map[*regexp.Regexp]int{
// EnvAWSFingerprint is used to fingerprint AWS metadata
type EnvAWSFingerprint struct {
StaticFingerprinter
timeout time.Duration
logger log.Logger

// endpoint for EC2 metadata as expected by AWS SDK
endpoint string

logger log.Logger
}

// NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata
func NewEnvAWSFingerprint(logger log.Logger) Fingerprint {
f := &EnvAWSFingerprint{
logger: logger.Named("env_aws"),
timeout: AwsMetadataTimeout,
logger: logger.Named("env_aws"),
endpoint: strings.TrimSuffix(os.Getenv("AWS_ENV_URL"), "/meta-data/"),
}
return f
}

func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error {
cfg := request.Config

timeout := AwsMetadataTimeout

// Check if we should tighten the timeout
if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
f.timeout = 1 * time.Millisecond
timeout = 1 * time.Millisecond
}

if !f.isAWS() {
ec2meta := ec2MetaClient(f.endpoint, timeout)

if !ec2meta.Available() {
return nil
}

Expand All @@ -80,16 +86,6 @@ func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *F
Device: "eth0",
}

metadataURL := os.Getenv("AWS_ENV_URL")
if metadataURL == "" {
metadataURL = DEFAULT_AWS_URL
}

client := &http.Client{
Timeout: f.timeout,
Transport: cleanhttp.DefaultTransport(),
}

// Keys and whether they should be namespaced as unique. Any key whose value
// uniquely identifies a node, such as ip, should be marked as unique. When
// marked as unique, the key isn't included in the computed node class.
Expand All @@ -105,23 +101,19 @@ func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *F
"placement/availability-zone": false,
}
for k, unique := range keys {
res, err := client.Get(metadataURL + k)
if err != nil {
resp, err := ec2meta.GetMetadata(k)
if awsErr, ok := err.(awserr.RequestFailure); ok {
f.logger.Debug("could not read attribute value", "attribute", k, "error", awsErr)
continue
} else if awsErr, ok := err.(awserr.Error); ok {
// if it's a URL error, assume we're not in an AWS environment
// TODO: better way to detect AWS? Check xen virtualization?
if _, ok := err.(*url.Error); ok {
if _, ok := awsErr.OrigErr().(*url.Error); ok {
return nil
}

// not sure what other errors it would return
return err
} else if res.StatusCode != http.StatusOK {
f.logger.Debug("could not read attribute value", "attribute", k)
continue
}
resp, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
f.logger.Error("error reading response body for AWS attribute", "attribute", k, "error", err)
}

// assume we want blank entries
Expand All @@ -130,7 +122,7 @@ func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *F
key = structs.UniqueNamespace(key)
}

response.AddAttribute(key, strings.Trim(string(resp), "\n"))
response.AddAttribute(key, strings.Trim(resp, "\n"))
}

// copy over network specific information
Expand All @@ -141,10 +133,11 @@ func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *F
}

// find LinkSpeed from lookup
throughput := f.linkSpeed()
if cfg.NetworkSpeed != 0 {
throughput = cfg.NetworkSpeed
} else if throughput == 0 {
throughput := cfg.NetworkSpeed
if throughput == 0 {
throughput = f.linkSpeed(ec2meta)
}
if throughput == 0 {
// Failed to determine speed. Check if the network fingerprint got it
found := false
if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 {
Expand Down Expand Up @@ -177,75 +170,16 @@ func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *F
return nil
}

func (f *EnvAWSFingerprint) isAWS() bool {
// Read the internal metadata URL from the environment, allowing test files to
// provide their own
metadataURL := os.Getenv("AWS_ENV_URL")
if metadataURL == "" {
metadataURL = DEFAULT_AWS_URL
}

client := &http.Client{
Timeout: f.timeout,
Transport: cleanhttp.DefaultTransport(),
}

// Query the metadata url for the ami-id, to verify we're on AWS
resp, err := client.Get(metadataURL + "ami-id")
if err != nil {
f.logger.Debug("error querying AWS Metadata URL, skipping")
return false
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
// URL not found, which indicates that this isn't AWS
return false
}

instanceID, err := ioutil.ReadAll(resp.Body)
if err != nil {
f.logger.Debug("error reading AWS Instance ID, skipping")
return false
}

match, err := regexp.MatchString("ami-*", string(instanceID))
if err != nil || !match {
return false
}

return true
}

// EnvAWSFingerprint uses lookup table to approximate network speeds
func (f *EnvAWSFingerprint) linkSpeed() int {

// Query the API for the instance type, and use the table above to approximate
// the network speed
metadataURL := os.Getenv("AWS_ENV_URL")
if metadataURL == "" {
metadataURL = DEFAULT_AWS_URL
}
func (f *EnvAWSFingerprint) linkSpeed(ec2meta *ec2metadata.EC2Metadata) int {

client := &http.Client{
Timeout: f.timeout,
Transport: cleanhttp.DefaultTransport(),
}

res, err := client.Get(metadataURL + "instance-type")