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 }