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