Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use AWS SDK to access EC2 Metadata #6779

Merged
merged 3 commits into from
Dec 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestFailures occur when we have a non-200 http code so it's the equivalent to res.StatusCode != http.StatusOK part below.

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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this TODO and check solved by the introduction of your ec2meta.Available() check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe so. ec2meta.Available() does the same check as before, namely check a metadata entry (instance-id or ami-id).

I'm not fully sure what this protect against and how we expect it to behave. If the earlier check Available metadata lookup succeeded, but looking up another metadata failed with a connection error, it feels to me we ought to retry rather than give up completely?!

Either way, I decided to leave this alone /shrug.

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")
resp, err := ec2meta.GetMetadata("instance-type")
if err != nil {
f.logger.Error("error reading instance-type", "error", err)
return 0
}

body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
f.logger.Error("error reading response body for instance-type", "error", err)
return 0
}

key := strings.Trim(string(body), "\n")
key := strings.Trim(resp, "\n")
netSpeed := 0
for reg, speed := range ec2InstanceSpeedMap {
if reg.MatchString(key) {
Expand All @@ -256,3 +190,16 @@ func (f *EnvAWSFingerprint) linkSpeed() int {

return netSpeed
}

func ec2MetaClient(endpoint string, timeout time.Duration) *ec2metadata.EC2Metadata {
client := &http.Client{
Timeout: timeout,
Transport: cleanhttp.DefaultTransport(),
}

c := aws.NewConfig().WithHTTPClient(client)
if endpoint != "" {
c = c.WithEndpoint(endpoint)
}
return ec2metadata.New(session.New(), c)
}
Loading