github.com/manicqin/nomad@v0.9.5/client/fingerprint/env_aws.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/aws/aws-sdk-go/aws"
    13  	"github.com/aws/aws-sdk-go/aws/awserr"
    14  	"github.com/aws/aws-sdk-go/aws/ec2metadata"
    15  	"github.com/aws/aws-sdk-go/aws/session"
    16  	log "github.com/hashicorp/go-hclog"
    17  
    18  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    19  	"github.com/hashicorp/nomad/nomad/structs"
    20  )
    21  
    22  const (
    23  	// AwsMetadataTimeout is the timeout used when contacting the AWS metadata
    24  	// service
    25  	AwsMetadataTimeout = 2 * time.Second
    26  )
    27  
    28  // map of instance type to approximate speed, in Mbits/s
    29  // Estimates from http://stackoverflow.com/a/35806587
    30  // This data is meant for a loose approximation
    31  var ec2InstanceSpeedMap = map[*regexp.Regexp]int{
    32  	regexp.MustCompile("t2.nano"):      30,
    33  	regexp.MustCompile("t2.micro"):     70,
    34  	regexp.MustCompile("t2.small"):     125,
    35  	regexp.MustCompile("t2.medium"):    300,
    36  	regexp.MustCompile("m3.medium"):    400,
    37  	regexp.MustCompile("c4.8xlarge"):   4000,
    38  	regexp.MustCompile("x1.16xlarge"):  5000,
    39  	regexp.MustCompile(`.*\.large`):    500,
    40  	regexp.MustCompile(`.*\.xlarge`):   750,
    41  	regexp.MustCompile(`.*\.2xlarge`):  1000,
    42  	regexp.MustCompile(`.*\.4xlarge`):  2000,
    43  	regexp.MustCompile(`.*\.8xlarge`):  10000,
    44  	regexp.MustCompile(`.*\.10xlarge`): 10000,
    45  	regexp.MustCompile(`.*\.16xlarge`): 10000,
    46  	regexp.MustCompile(`.*\.32xlarge`): 10000,
    47  }
    48  
    49  // EnvAWSFingerprint is used to fingerprint AWS metadata
    50  type EnvAWSFingerprint struct {
    51  	StaticFingerprinter
    52  
    53  	// endpoint for EC2 metadata as expected by AWS SDK
    54  	endpoint string
    55  
    56  	logger log.Logger
    57  }
    58  
    59  // NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata
    60  func NewEnvAWSFingerprint(logger log.Logger) Fingerprint {
    61  	f := &EnvAWSFingerprint{
    62  		logger:   logger.Named("env_aws"),
    63  		endpoint: strings.TrimSuffix(os.Getenv("AWS_ENV_URL"), "/meta-data/"),
    64  	}
    65  	return f
    66  }
    67  
    68  func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error {
    69  	cfg := request.Config
    70  
    71  	timeout := AwsMetadataTimeout
    72  
    73  	// Check if we should tighten the timeout
    74  	if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
    75  		timeout = 1 * time.Millisecond
    76  	}
    77  
    78  	ec2meta, err := ec2MetaClient(f.endpoint, timeout)
    79  	if err != nil {
    80  		return fmt.Errorf("failed to setup ec2Metadata client: %v", err)
    81  	}
    82  
    83  	if !ec2meta.Available() {
    84  		return nil
    85  	}
    86  
    87  	// newNetwork is populated and added to the Nodes resources
    88  	newNetwork := &structs.NetworkResource{
    89  		Device: "eth0",
    90  	}
    91  
    92  	// Keys and whether they should be namespaced as unique. Any key whose value
    93  	// uniquely identifies a node, such as ip, should be marked as unique. When
    94  	// marked as unique, the key isn't included in the computed node class.
    95  	keys := map[string]bool{
    96  		"ami-id":                      false,
    97  		"hostname":                    true,
    98  		"instance-id":                 true,
    99  		"instance-type":               false,
   100  		"local-hostname":              true,
   101  		"local-ipv4":                  true,
   102  		"public-hostname":             true,
   103  		"public-ipv4":                 true,
   104  		"placement/availability-zone": false,
   105  	}
   106  	for k, unique := range keys {
   107  		resp, err := ec2meta.GetMetadata(k)
   108  		if awsErr, ok := err.(awserr.RequestFailure); ok {
   109  			f.logger.Debug("could not read attribute value", "attribute", k, "error", awsErr)
   110  			continue
   111  		} else if awsErr, ok := err.(awserr.Error); ok {
   112  			// if it's a URL error, assume we're not in an AWS environment
   113  			// TODO: better way to detect AWS? Check xen virtualization?
   114  			if _, ok := awsErr.OrigErr().(*url.Error); ok {
   115  				return nil
   116  			}
   117  
   118  			// not sure what other errors it would return
   119  			return err
   120  		}
   121  
   122  		// assume we want blank entries
   123  		key := "platform.aws." + strings.Replace(k, "/", ".", -1)
   124  		if unique {
   125  			key = structs.UniqueNamespace(key)
   126  		}
   127  
   128  		response.AddAttribute(key, strings.Trim(resp, "\n"))
   129  	}
   130  
   131  	// copy over network specific information
   132  	if val, ok := response.Attributes["unique.platform.aws.local-ipv4"]; ok && val != "" {
   133  		response.AddAttribute("unique.network.ip-address", val)
   134  		newNetwork.IP = val
   135  		newNetwork.CIDR = newNetwork.IP + "/32"
   136  	}
   137  
   138  	// find LinkSpeed from lookup
   139  	throughput := cfg.NetworkSpeed
   140  	if throughput == 0 {
   141  		throughput = f.linkSpeed(ec2meta)
   142  	}
   143  	if throughput == 0 {
   144  		// Failed to determine speed. Check if the network fingerprint got it
   145  		found := false
   146  		if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 {
   147  			for _, n := range request.Node.Resources.Networks {
   148  				if n.IP == newNetwork.IP {
   149  					throughput = n.MBits
   150  					found = true
   151  					break
   152  				}
   153  			}
   154  		}
   155  
   156  		// Nothing detected so default
   157  		if !found {
   158  			throughput = defaultNetworkSpeed
   159  		}
   160  	}
   161  
   162  	newNetwork.MBits = throughput
   163  	response.NodeResources = &structs.NodeResources{
   164  		Networks: []*structs.NetworkResource{newNetwork},
   165  	}
   166  
   167  	// populate Links
   168  	response.AddLink("aws.ec2", fmt.Sprintf("%s.%s",
   169  		response.Attributes["platform.aws.placement.availability-zone"],
   170  		response.Attributes["unique.platform.aws.instance-id"]))
   171  	response.Detected = true
   172  
   173  	return nil
   174  }
   175  
   176  // EnvAWSFingerprint uses lookup table to approximate network speeds
   177  func (f *EnvAWSFingerprint) linkSpeed(ec2meta *ec2metadata.EC2Metadata) int {
   178  
   179  	resp, err := ec2meta.GetMetadata("instance-type")
   180  	if err != nil {
   181  		f.logger.Error("error reading instance-type", "error", err)
   182  		return 0
   183  	}
   184  
   185  	key := strings.Trim(resp, "\n")
   186  	netSpeed := 0
   187  	for reg, speed := range ec2InstanceSpeedMap {
   188  		if reg.MatchString(key) {
   189  			netSpeed = speed
   190  			break
   191  		}
   192  	}
   193  
   194  	return netSpeed
   195  }
   196  
   197  func ec2MetaClient(endpoint string, timeout time.Duration) (*ec2metadata.EC2Metadata, error) {
   198  	client := &http.Client{
   199  		Timeout:   timeout,
   200  		Transport: cleanhttp.DefaultTransport(),
   201  	}
   202  
   203  	c := aws.NewConfig().WithHTTPClient(client).WithMaxRetries(0)
   204  	if endpoint != "" {
   205  		c = c.WithEndpoint(endpoint)
   206  	}
   207  
   208  	session, err := session.NewSession(c)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	return ec2metadata.New(session, c), nil
   213  }