github.com/bigcommerce/nomad@v0.9.3-bc/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 err != nil { 110 // if it's a URL error, assume we're not in an AWS environment 111 // TODO: better way to detect AWS? Check xen virtualization? 112 if _, ok := err.(*url.Error); ok { 113 return nil 114 } 115 // not sure what other errors it would return 116 return err 117 } else if res.StatusCode != http.StatusOK { 118 f.logger.Debug("could not read attribute value", "attribute", k) 119 continue 120 } 121 resp, err := ioutil.ReadAll(res.Body) 122 res.Body.Close() 123 if err != nil { 124 f.logger.Error("error reading response body for AWS attribute", "attribute", k, "error", err) 125 } 126 127 // assume we want blank entries 128 key := "platform.aws." + strings.Replace(k, "/", ".", -1) 129 if unique { 130 key = structs.UniqueNamespace(key) 131 } 132 133 response.AddAttribute(key, strings.Trim(string(resp), "\n")) 134 } 135 136 // copy over network specific information 137 if val, ok := response.Attributes["unique.platform.aws.local-ipv4"]; ok && val != "" { 138 response.AddAttribute("unique.network.ip-address", val) 139 newNetwork.IP = val 140 newNetwork.CIDR = newNetwork.IP + "/32" 141 } 142 143 // find LinkSpeed from lookup 144 throughput := f.linkSpeed() 145 if cfg.NetworkSpeed != 0 { 146 throughput = cfg.NetworkSpeed 147 } else if throughput == 0 { 148 // Failed to determine speed. Check if the network fingerprint got it 149 found := false 150 if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 { 151 for _, n := range request.Node.Resources.Networks { 152 if n.IP == newNetwork.IP { 153 throughput = n.MBits 154 found = true 155 break 156 } 157 } 158 } 159 160 // Nothing detected so default 161 if !found { 162 throughput = defaultNetworkSpeed 163 } 164 } 165 166 newNetwork.MBits = throughput 167 response.NodeResources = &structs.NodeResources{ 168 Networks: []*structs.NetworkResource{newNetwork}, 169 } 170 171 // populate Links 172 response.AddLink("aws.ec2", fmt.Sprintf("%s.%s", 173 response.Attributes["platform.aws.placement.availability-zone"], 174 response.Attributes["unique.platform.aws.instance-id"])) 175 response.Detected = true 176 177 return nil 178 } 179 180 func (f *EnvAWSFingerprint) isAWS() bool { 181 // Read the internal metadata URL from the environment, allowing test files to 182 // provide their own 183 metadataURL := os.Getenv("AWS_ENV_URL") 184 if metadataURL == "" { 185 metadataURL = DEFAULT_AWS_URL 186 } 187 188 client := &http.Client{ 189 Timeout: f.timeout, 190 Transport: cleanhttp.DefaultTransport(), 191 } 192 193 // Query the metadata url for the ami-id, to verify we're on AWS 194 resp, err := client.Get(metadataURL + "ami-id") 195 if err != nil { 196 f.logger.Debug("error querying AWS Metadata URL, skipping") 197 return false 198 } 199 defer resp.Body.Close() 200 201 if resp.StatusCode >= 400 { 202 // URL not found, which indicates that this isn't AWS 203 return false 204 } 205 206 instanceID, err := ioutil.ReadAll(resp.Body) 207 if err != nil { 208 f.logger.Debug("error reading AWS Instance ID, skipping") 209 return false 210 } 211 212 match, err := regexp.MatchString("ami-*", string(instanceID)) 213 if err != nil || !match { 214 return false 215 } 216 217 return true 218 } 219 220 // EnvAWSFingerprint uses lookup table to approximate network speeds 221 func (f *EnvAWSFingerprint) linkSpeed() int { 222 223 // Query the API for the instance type, and use the table above to approximate 224 // the network speed 225 metadataURL := os.Getenv("AWS_ENV_URL") 226 if metadataURL == "" { 227 metadataURL = DEFAULT_AWS_URL 228 } 229 230 client := &http.Client{ 231 Timeout: f.timeout, 232 Transport: cleanhttp.DefaultTransport(), 233 } 234 235 res, err := client.Get(metadataURL + "instance-type") 236 if err != nil { 237 f.logger.Error("error reading instance-type", "error", err) 238 return 0 239 } 240 241 body, err := ioutil.ReadAll(res.Body) 242 res.Body.Close() 243 if err != nil { 244 f.logger.Error("error reading response body for instance-type", "error", err) 245 return 0 246 } 247 248 key := strings.Trim(string(body), "\n") 249 netSpeed := 0 250 for reg, speed := range ec2InstanceSpeedMap { 251 if reg.MatchString(key) { 252 netSpeed = speed 253 break 254 } 255 } 256 257 return netSpeed 258 }