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