github.com/smithx10/nomad@v0.9.1-rc1/client/fingerprint/env_aws.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/http"
     7  	"net/url"
     8  	"os"
     9  	"regexp"
    10  	"strings"
    11  	"time"
    12  
    13  	log "github.com/hashicorp/go-hclog"
    14  
    15  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    16  	"github.com/hashicorp/nomad/nomad/structs"
    17  )
    18  
    19  const (
    20  	// This is where the AWS metadata server normally resides. We hardcode the
    21  	// "instance" path as well since it's the only one we access here.
    22  	DEFAULT_AWS_URL = "http://169.254.169.254/latest/meta-data/"
    23  
    24  	// AwsMetadataTimeout is the timeout used when contacting the AWS metadata
    25  	// service
    26  	AwsMetadataTimeout = 2 * time.Second
    27  )
    28  
    29  // map of instance type to approximate speed, in Mbits/s
    30  // Estimates from http://stackoverflow.com/a/35806587
    31  // This data is meant for a loose approximation
    32  var ec2InstanceSpeedMap = map[*regexp.Regexp]int{
    33  	regexp.MustCompile("t2.nano"):      30,
    34  	regexp.MustCompile("t2.micro"):     70,
    35  	regexp.MustCompile("t2.small"):     125,
    36  	regexp.MustCompile("t2.medium"):    300,
    37  	regexp.MustCompile("m3.medium"):    400,
    38  	regexp.MustCompile("c4.8xlarge"):   4000,
    39  	regexp.MustCompile("x1.16xlarge"):  5000,
    40  	regexp.MustCompile(`.*\.large`):    500,
    41  	regexp.MustCompile(`.*\.xlarge`):   750,
    42  	regexp.MustCompile(`.*\.2xlarge`):  1000,
    43  	regexp.MustCompile(`.*\.4xlarge`):  2000,
    44  	regexp.MustCompile(`.*\.8xlarge`):  10000,
    45  	regexp.MustCompile(`.*\.10xlarge`): 10000,
    46  	regexp.MustCompile(`.*\.16xlarge`): 10000,
    47  	regexp.MustCompile(`.*\.32xlarge`): 10000,
    48  }
    49  
    50  // EnvAWSFingerprint is used to fingerprint AWS metadata
    51  type EnvAWSFingerprint struct {
    52  	StaticFingerprinter
    53  	timeout time.Duration
    54  	logger  log.Logger
    55  }
    56  
    57  // NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata
    58  func NewEnvAWSFingerprint(logger log.Logger) Fingerprint {
    59  	f := &EnvAWSFingerprint{
    60  		logger:  logger.Named("env_aws"),
    61  		timeout: AwsMetadataTimeout,
    62  	}
    63  	return f
    64  }
    65  
    66  func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error {
    67  	cfg := request.Config
    68  
    69  	// Check if we should tighten the timeout
    70  	if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
    71  		f.timeout = 1 * time.Millisecond
    72  	}
    73  
    74  	if !f.isAWS() {
    75  		return nil
    76  	}
    77  
    78  	// newNetwork is populated and added to the Nodes resources
    79  	newNetwork := &structs.NetworkResource{
    80  		Device: "eth0",
    81  	}
    82  
    83  	metadataURL := os.Getenv("AWS_ENV_URL")
    84  	if metadataURL == "" {
    85  		metadataURL = DEFAULT_AWS_URL
    86  	}
    87  
    88  	client := &http.Client{
    89  		Timeout:   f.timeout,
    90  		Transport: cleanhttp.DefaultTransport(),
    91  	}
    92  
    93  	// Keys and whether they should be namespaced as unique. Any key whose value
    94  	// uniquely identifies a node, such as ip, should be marked as unique. When
    95  	// marked as unique, the key isn't included in the computed node class.
    96  	keys := map[string]bool{
    97  		"ami-id":                      false,
    98  		"hostname":                    true,
    99  		"instance-id":                 true,
   100  		"instance-type":               false,
   101  		"local-hostname":              true,
   102  		"local-ipv4":                  true,
   103  		"public-hostname":             true,
   104  		"public-ipv4":                 true,
   105  		"placement/availability-zone": false,
   106  	}
   107  	for k, unique := range keys {
   108  		res, err := client.Get(metadataURL + k)
   109  		if res.StatusCode != http.StatusOK {
   110  			f.logger.Debug("could not read attribute value", "attribute", k)
   111  			continue
   112  		}
   113  		if err != nil {
   114  			// if it's a URL error, assume we're not in an AWS environment
   115  			// TODO: better way to detect AWS? Check xen virtualization?
   116  			if _, ok := err.(*url.Error); ok {
   117  				return nil
   118  			}
   119  			// not sure what other errors it would return
   120  			return err
   121  		}
   122  		resp, err := ioutil.ReadAll(res.Body)
   123  		res.Body.Close()
   124  		if err != nil {
   125  			f.logger.Error("error reading response body for AWS attribute", "attribute", k, "error", err)
   126  		}
   127  
   128  		// assume we want blank entries
   129  		key := "platform.aws." + strings.Replace(k, "/", ".", -1)
   130  		if unique {
   131  			key = structs.UniqueNamespace(key)
   132  		}
   133  
   134  		response.AddAttribute(key, strings.Trim(string(resp), "\n"))
   135  	}
   136  
   137  	// copy over network specific information
   138  	if val, ok := response.Attributes["unique.platform.aws.local-ipv4"]; ok && val != "" {
   139  		response.AddAttribute("unique.network.ip-address", val)
   140  		newNetwork.IP = val
   141  		newNetwork.CIDR = newNetwork.IP + "/32"
   142  	}
   143  
   144  	// find LinkSpeed from lookup
   145  	throughput := f.linkSpeed()
   146  	if cfg.NetworkSpeed != 0 {
   147  		throughput = cfg.NetworkSpeed
   148  	} else if throughput == 0 {
   149  		// Failed to determine speed. Check if the network fingerprint got it
   150  		found := false
   151  		if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 {
   152  			for _, n := range request.Node.Resources.Networks {
   153  				if n.IP == newNetwork.IP {
   154  					throughput = n.MBits
   155  					found = true
   156  					break
   157  				}
   158  			}
   159  		}
   160  
   161  		// Nothing detected so default
   162  		if !found {
   163  			throughput = defaultNetworkSpeed
   164  		}
   165  	}
   166  
   167  	newNetwork.MBits = throughput
   168  	response.NodeResources = &structs.NodeResources{
   169  		Networks: []*structs.NetworkResource{newNetwork},
   170  	}
   171  
   172  	// populate Links
   173  	response.AddLink("aws.ec2", fmt.Sprintf("%s.%s",
   174  		response.Attributes["platform.aws.placement.availability-zone"],
   175  		response.Attributes["unique.platform.aws.instance-id"]))
   176  	response.Detected = true
   177  
   178  	return nil
   179  }
   180  
   181  func (f *EnvAWSFingerprint) isAWS() bool {
   182  	// Read the internal metadata URL from the environment, allowing test files to
   183  	// provide their own
   184  	metadataURL := os.Getenv("AWS_ENV_URL")
   185  	if metadataURL == "" {
   186  		metadataURL = DEFAULT_AWS_URL
   187  	}
   188  
   189  	client := &http.Client{
   190  		Timeout:   f.timeout,
   191  		Transport: cleanhttp.DefaultTransport(),
   192  	}
   193  
   194  	// Query the metadata url for the ami-id, to verify we're on AWS
   195  	resp, err := client.Get(metadataURL + "ami-id")
   196  	if err != nil {
   197  		f.logger.Debug("error querying AWS Metadata URL, skipping")
   198  		return false
   199  	}
   200  	defer resp.Body.Close()
   201  
   202  	if resp.StatusCode >= 400 {
   203  		// URL not found, which indicates that this isn't AWS
   204  		return false
   205  	}
   206  
   207  	instanceID, err := ioutil.ReadAll(resp.Body)
   208  	if err != nil {
   209  		f.logger.Debug("error reading AWS Instance ID, skipping")
   210  		return false
   211  	}
   212  
   213  	match, err := regexp.MatchString("ami-*", string(instanceID))
   214  	if err != nil || !match {
   215  		return false
   216  	}
   217  
   218  	return true
   219  }
   220  
   221  // EnvAWSFingerprint uses lookup table to approximate network speeds
   222  func (f *EnvAWSFingerprint) linkSpeed() int {
   223  
   224  	// Query the API for the instance type, and use the table above to approximate
   225  	// the network speed
   226  	metadataURL := os.Getenv("AWS_ENV_URL")
   227  	if metadataURL == "" {
   228  		metadataURL = DEFAULT_AWS_URL
   229  	}
   230  
   231  	client := &http.Client{
   232  		Timeout:   f.timeout,
   233  		Transport: cleanhttp.DefaultTransport(),
   234  	}
   235  
   236  	res, err := client.Get(metadataURL + "instance-type")
   237  	if err != nil {
   238  		f.logger.Error("error reading instance-type", "error", err)
   239  		return 0
   240  	}
   241  
   242  	body, err := ioutil.ReadAll(res.Body)
   243  	res.Body.Close()
   244  	if err != nil {
   245  		f.logger.Error("error reading response body for instance-type", "error", err)
   246  		return 0
   247  	}
   248  
   249  	key := strings.Trim(string(body), "\n")
   250  	netSpeed := 0
   251  	for reg, speed := range ec2InstanceSpeedMap {
   252  		if reg.MatchString(key) {
   253  			netSpeed = speed
   254  			break
   255  		}
   256  	}
   257  
   258  	return netSpeed
   259  }