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