github.com/kardianos/nomad@v0.1.3-0.20151022182107-b13df73ee850/client/fingerprint/env_aws.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"io/ioutil"
     5  	"log"
     6  	"net/http"
     7  	"net/url"
     8  	"os"
     9  	"regexp"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/hashicorp/go-cleanhttp"
    14  	"github.com/hashicorp/nomad/client/config"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  )
    17  
    18  // map of instance type to approximate speed, in Mbits/s
    19  // http://serverfault.com/questions/324883/aws-bandwidth-and-content-delivery/326797#326797
    20  // which itself cites these sources:
    21  // - http://blog.rightscale.com/2007/10/28/network-performance-within-amazon-ec2-and-to-amazon-s3/
    22  // - http://www.soc.napier.ac.uk/~bill/chris_p.pdf
    23  //
    24  // This data is meant for a loose approximation
    25  var ec2InstanceSpeedMap = map[string]int{
    26  	"m4.large":    80,
    27  	"m3.medium":   80,
    28  	"m3.large":    80,
    29  	"c4.large":    80,
    30  	"c3.large":    80,
    31  	"c3.xlarge":   80,
    32  	"r3.large":    80,
    33  	"r3.xlarge":   80,
    34  	"i2.xlarge":   80,
    35  	"d2.xlarge":   80,
    36  	"t2.micro":    16,
    37  	"t2.small":    16,
    38  	"t2.medium":   16,
    39  	"t2.large":    16,
    40  	"m4.xlarge":   760,
    41  	"m4.2xlarge":  760,
    42  	"m4.4xlarge":  760,
    43  	"m3.xlarge":   760,
    44  	"m3.2xlarge":  760,
    45  	"c4.xlarge":   760,
    46  	"c4.2xlarge":  760,
    47  	"c4.4xlarge":  760,
    48  	"c3.2xlarge":  760,
    49  	"c3.4xlarge":  760,
    50  	"g2.2xlarge":  760,
    51  	"r3.2xlarge":  760,
    52  	"r3.4xlarge":  760,
    53  	"i2.2xlarge":  760,
    54  	"i2.4xlarge":  760,
    55  	"d2.2xlarge":  760,
    56  	"d2.4xlarge":  760,
    57  	"m4.10xlarge": 10000,
    58  	"c4.8xlarge":  10000,
    59  	"c3.8xlarge":  10000,
    60  	"g2.8xlarge":  10000,
    61  	"r3.8xlarge":  10000,
    62  	"i2.8xlarge":  10000,
    63  	"d2.8xlarge":  10000,
    64  }
    65  
    66  // EnvAWSFingerprint is used to fingerprint AWS metadata
    67  type EnvAWSFingerprint struct {
    68  	logger *log.Logger
    69  }
    70  
    71  // NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata
    72  func NewEnvAWSFingerprint(logger *log.Logger) Fingerprint {
    73  	f := &EnvAWSFingerprint{logger: logger}
    74  	return f
    75  }
    76  
    77  func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
    78  	if !isAWS() {
    79  		return false, nil
    80  	}
    81  
    82  	// newNetwork is populated and addded to the Nodes resources
    83  	newNetwork := &structs.NetworkResource{
    84  		Device: "eth0",
    85  	}
    86  
    87  	if node.Links == nil {
    88  		node.Links = make(map[string]string)
    89  	}
    90  	metadataURL := os.Getenv("AWS_ENV_URL")
    91  	if metadataURL == "" {
    92  		metadataURL = "http://169.254.169.254/latest/meta-data/"
    93  	}
    94  
    95  	// assume 2 seconds is enough time for inside AWS network
    96  	client := &http.Client{
    97  		Timeout:   2 * time.Second,
    98  		Transport: cleanhttp.DefaultTransport(),
    99  	}
   100  
   101  	keys := []string{
   102  		"ami-id",
   103  		"hostname",
   104  		"instance-id",
   105  		"instance-type",
   106  		"local-hostname",
   107  		"local-ipv4",
   108  		"public-hostname",
   109  		"public-ipv4",
   110  		"placement/availability-zone",
   111  	}
   112  	for _, k := range keys {
   113  		res, err := client.Get(metadataURL + k)
   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 := strings.Replace(k, "/", ".", -1)
   131  		node.Attributes["platform.aws."+key] = strings.Trim(string(resp), "\n")
   132  	}
   133  
   134  	// copy over network specific information
   135  	if node.Attributes["platform.aws.local-ipv4"] != "" {
   136  		node.Attributes["network.ip-address"] = node.Attributes["platform.aws.local-ipv4"]
   137  		newNetwork.IP = node.Attributes["platform.aws.local-ipv4"]
   138  		newNetwork.CIDR = newNetwork.IP + "/32"
   139  	}
   140  
   141  	// find LinkSpeed from lookup
   142  	if throughput := f.linkSpeed(); throughput > 0 {
   143  		newNetwork.MBits = throughput
   144  	}
   145  
   146  	if node.Resources == nil {
   147  		node.Resources = &structs.Resources{}
   148  	}
   149  	node.Resources.Networks = append(node.Resources.Networks, newNetwork)
   150  
   151  	// populate Node Network Resources
   152  
   153  	// populate Links
   154  	node.Links["aws.ec2"] = node.Attributes["platform.aws.placement.availability-zone"] + "." + node.Attributes["platform.aws.instance-id"]
   155  
   156  	return true, nil
   157  }
   158  
   159  func isAWS() bool {
   160  	// Read the internal metadata URL from the environment, allowing test files to
   161  	// provide their own
   162  	metadataURL := os.Getenv("AWS_ENV_URL")
   163  	if metadataURL == "" {
   164  		metadataURL = "http://169.254.169.254/latest/meta-data/"
   165  	}
   166  
   167  	// assume 2 seconds is enough time for inside AWS network
   168  	client := &http.Client{
   169  		Timeout:   2 * time.Second,
   170  		Transport: cleanhttp.DefaultTransport(),
   171  	}
   172  
   173  	// Query the metadata url for the ami-id, to veryify we're on AWS
   174  	resp, err := client.Get(metadataURL + "ami-id")
   175  
   176  	if err != nil {
   177  		log.Printf("[ERR] fingerprint.env_aws: Error querying AWS Metadata URL, skipping")
   178  		return false
   179  	}
   180  	defer resp.Body.Close()
   181  
   182  	if resp.StatusCode >= 400 {
   183  		// URL not found, which indicates that this isn't AWS
   184  		return false
   185  	}
   186  
   187  	instanceID, err := ioutil.ReadAll(resp.Body)
   188  	if err != nil {
   189  		log.Printf("[ERR] fingerprint.env_aws: Error reading AWS Instance ID, skipping")
   190  		return false
   191  	}
   192  
   193  	match, err := regexp.MatchString("ami-*", string(instanceID))
   194  	if !match {
   195  		return false
   196  	}
   197  
   198  	return true
   199  }
   200  
   201  // EnvAWSFingerprint uses lookup table to approximate network speeds
   202  func (f *EnvAWSFingerprint) linkSpeed() int {
   203  
   204  	// Query the API for the instance type, and use the table above to approximate
   205  	// the network speed
   206  	metadataURL := os.Getenv("AWS_ENV_URL")
   207  	if metadataURL == "" {
   208  		metadataURL = "http://169.254.169.254/latest/meta-data/"
   209  	}
   210  
   211  	// assume 2 seconds is enough time for inside AWS network
   212  	client := &http.Client{
   213  		Timeout:   2 * time.Second,
   214  		Transport: cleanhttp.DefaultTransport(),
   215  	}
   216  
   217  	res, err := client.Get(metadataURL + "instance-type")
   218  	body, err := ioutil.ReadAll(res.Body)
   219  	res.Body.Close()
   220  	if err != nil {
   221  		f.logger.Printf("[ERR]: fingerprint.env_aws: Error reading response body for instance-type")
   222  		return 0
   223  	}
   224  
   225  	key := strings.Trim(string(body), "\n")
   226  	v, ok := ec2InstanceSpeedMap[key]
   227  	if !ok {
   228  		return 0
   229  	}
   230  
   231  	return v
   232  }