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