github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/client/fingerprint/env_aws.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"log"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/hashicorp/go-cleanhttp"
    15  	"github.com/hashicorp/nomad/client/config"
    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,
    61  		timeout: AwsMetadataTimeout,
    62  	}
    63  	return f
    64  }
    65  
    66  func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
    67  	// Check if we should tighten the timeout
    68  	if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
    69  		f.timeout = 1 * time.Millisecond
    70  	}
    71  
    72  	if !f.isAWS() {
    73  		return false, nil
    74  	}
    75  
    76  	// newNetwork is populated and addded to the Nodes resources
    77  	newNetwork := &structs.NetworkResource{
    78  		Device: "eth0",
    79  	}
    80  
    81  	if node.Links == nil {
    82  		node.Links = make(map[string]string)
    83  	}
    84  	metadataURL := os.Getenv("AWS_ENV_URL")
    85  	if metadataURL == "" {
    86  		metadataURL = DEFAULT_AWS_URL
    87  	}
    88  
    89  	client := &http.Client{
    90  		Timeout:   f.timeout,
    91  		Transport: cleanhttp.DefaultTransport(),
    92  	}
    93  
    94  	// Keys and whether they should be namespaced as unique. Any key whose value
    95  	// uniquely identifies a node, such as ip, should be marked as unique. When
    96  	// marked as unique, the key isn't included in the computed node class.
    97  	keys := map[string]bool{
    98  		"ami-id":                      true,
    99  		"hostname":                    true,
   100  		"instance-id":                 true,
   101  		"instance-type":               false,
   102  		"local-hostname":              true,
   103  		"local-ipv4":                  true,
   104  		"public-hostname":             true,
   105  		"public-ipv4":                 true,
   106  		"placement/availability-zone": false,
   107  	}
   108  	for k, unique := range keys {
   109  		res, err := client.Get(metadataURL + k)
   110  		if res.StatusCode != http.StatusOK {
   111  			f.logger.Printf("[WARN]: fingerprint.env_aws: Could not read value for attribute %q", k)
   112  			continue
   113  		}
   114  		if err != nil {
   115  			// if it's a URL error, assume we're not in an AWS environment
   116  			// TODO: better way to detect AWS? Check xen virtualization?
   117  			if _, ok := err.(*url.Error); ok {
   118  				return false, nil
   119  			}
   120  			// not sure what other errors it would return
   121  			return false, err
   122  		}
   123  		resp, err := ioutil.ReadAll(res.Body)
   124  		res.Body.Close()
   125  		if err != nil {
   126  			f.logger.Printf("[ERR]: fingerprint.env_aws: Error reading response body for AWS %s", k)
   127  		}
   128  
   129  		// assume we want blank entries
   130  		key := "platform.aws." + strings.Replace(k, "/", ".", -1)
   131  		if unique {
   132  			key = structs.UniqueNamespace(key)
   133  		}
   134  
   135  		node.Attributes[key] = strings.Trim(string(resp), "\n")
   136  	}
   137  
   138  	// copy over network specific information
   139  	if val := node.Attributes["unique.platform.aws.local-ipv4"]; val != "" {
   140  		node.Attributes["unique.network.ip-address"] = val
   141  		newNetwork.IP = val
   142  		newNetwork.CIDR = newNetwork.IP + "/32"
   143  	}
   144  
   145  	// find LinkSpeed from lookup
   146  	throughput := f.linkSpeed()
   147  	if cfg.NetworkSpeed != 0 {
   148  		throughput = cfg.NetworkSpeed
   149  	} else if throughput == 0 {
   150  		// Failed to determine speed. Check if the network fingerprint got it
   151  		found := false
   152  		if node.Resources != nil && len(node.Resources.Networks) > 0 {
   153  			for _, n := range node.Resources.Networks {
   154  				if n.IP == newNetwork.IP {
   155  					throughput = n.MBits
   156  					found = true
   157  					break
   158  				}
   159  			}
   160  		}
   161  
   162  		// Nothing detected so default
   163  		if !found {
   164  			throughput = defaultNetworkSpeed
   165  		}
   166  	}
   167  
   168  	// populate Node Network Resources
   169  	if node.Resources == nil {
   170  		node.Resources = &structs.Resources{}
   171  	}
   172  	newNetwork.MBits = throughput
   173  	node.Resources.Networks = []*structs.NetworkResource{newNetwork}
   174  
   175  	// populate Links
   176  	node.Links["aws.ec2"] = fmt.Sprintf("%s.%s",
   177  		node.Attributes["platform.aws.placement.availability-zone"],
   178  		node.Attributes["unique.platform.aws.instance-id"])
   179  
   180  	return true, nil
   181  }
   182  
   183  func (f *EnvAWSFingerprint) isAWS() bool {
   184  	// Read the internal metadata URL from the environment, allowing test files to
   185  	// provide their own
   186  	metadataURL := os.Getenv("AWS_ENV_URL")
   187  	if metadataURL == "" {
   188  		metadataURL = DEFAULT_AWS_URL
   189  	}
   190  
   191  	client := &http.Client{
   192  		Timeout:   f.timeout,
   193  		Transport: cleanhttp.DefaultTransport(),
   194  	}
   195  
   196  	// Query the metadata url for the ami-id, to veryify we're on AWS
   197  	resp, err := client.Get(metadataURL + "ami-id")
   198  	if err != nil {
   199  		f.logger.Printf("[DEBUG] fingerprint.env_aws: Error querying AWS Metadata URL, skipping")
   200  		return false
   201  	}
   202  	defer resp.Body.Close()
   203  
   204  	if resp.StatusCode >= 400 {
   205  		// URL not found, which indicates that this isn't AWS
   206  		return false
   207  	}
   208  
   209  	instanceID, err := ioutil.ReadAll(resp.Body)
   210  	if err != nil {
   211  		f.logger.Printf("[DEBUG] fingerprint.env_aws: Error reading AWS Instance ID, skipping")
   212  		return false
   213  	}
   214  
   215  	match, err := regexp.MatchString("ami-*", string(instanceID))
   216  	if err != nil || !match {
   217  		return false
   218  	}
   219  
   220  	return true
   221  }
   222  
   223  // EnvAWSFingerprint uses lookup table to approximate network speeds
   224  func (f *EnvAWSFingerprint) linkSpeed() int {
   225  
   226  	// Query the API for the instance type, and use the table above to approximate
   227  	// the network speed
   228  	metadataURL := os.Getenv("AWS_ENV_URL")
   229  	if metadataURL == "" {
   230  		metadataURL = DEFAULT_AWS_URL
   231  	}
   232  
   233  	client := &http.Client{
   234  		Timeout:   f.timeout,
   235  		Transport: cleanhttp.DefaultTransport(),
   236  	}
   237  
   238  	res, err := client.Get(metadataURL + "instance-type")
   239  	body, err := ioutil.ReadAll(res.Body)
   240  	res.Body.Close()
   241  	if err != nil {
   242  		f.logger.Printf("[ERR]: fingerprint.env_aws: Error reading response body for instance-type")
   243  		return 0
   244  	}
   245  
   246  	key := strings.Trim(string(body), "\n")
   247  	netSpeed := 0
   248  	for reg, speed := range ec2InstanceSpeedMap {
   249  		if reg.MatchString(key) {
   250  			netSpeed = speed
   251  			break
   252  		}
   253  	}
   254  
   255  	return netSpeed
   256  }