github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/fingerprint/env_aws.go (about) 1 package fingerprint 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "os" 8 "regexp" 9 "strings" 10 "time" 11 12 "github.com/aws/aws-sdk-go/aws" 13 "github.com/aws/aws-sdk-go/aws/awserr" 14 "github.com/aws/aws-sdk-go/aws/ec2metadata" 15 "github.com/aws/aws-sdk-go/aws/session" 16 log "github.com/hashicorp/go-hclog" 17 18 cleanhttp "github.com/hashicorp/go-cleanhttp" 19 "github.com/hashicorp/nomad/nomad/structs" 20 ) 21 22 const ( 23 // AwsMetadataTimeout is the timeout used when contacting the AWS metadata 24 // services. 25 AwsMetadataTimeout = 2 * time.Second 26 ) 27 28 // map of instance type to approximate speed, in Mbits/s 29 // Estimates from http://stackoverflow.com/a/35806587 30 // This data is meant for a loose approximation 31 var ec2NetSpeedTable = map[*regexp.Regexp]int{ 32 regexp.MustCompile("t2.nano"): 30, 33 regexp.MustCompile("t2.micro"): 70, 34 regexp.MustCompile("t2.small"): 125, 35 regexp.MustCompile("t2.medium"): 300, 36 regexp.MustCompile("m3.medium"): 400, 37 regexp.MustCompile("c4.8xlarge"): 4000, 38 regexp.MustCompile("x1.16xlarge"): 5000, 39 regexp.MustCompile(`.*\.large`): 500, 40 regexp.MustCompile(`.*\.xlarge`): 750, 41 regexp.MustCompile(`.*\.2xlarge`): 1000, 42 regexp.MustCompile(`.*\.4xlarge`): 2000, 43 regexp.MustCompile(`.*\.8xlarge`): 10000, 44 regexp.MustCompile(`.*\.10xlarge`): 10000, 45 regexp.MustCompile(`.*\.16xlarge`): 10000, 46 regexp.MustCompile(`.*\.32xlarge`): 10000, 47 } 48 49 // EnvAWSFingerprint is used to fingerprint AWS metadata 50 type EnvAWSFingerprint struct { 51 StaticFingerprinter 52 53 // endpoint for EC2 metadata as expected by AWS SDK 54 endpoint string 55 56 logger log.Logger 57 } 58 59 // NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata 60 func NewEnvAWSFingerprint(logger log.Logger) Fingerprint { 61 f := &EnvAWSFingerprint{ 62 logger: logger.Named("env_aws"), 63 endpoint: strings.TrimSuffix(os.Getenv("AWS_ENV_URL"), "/meta-data/"), 64 } 65 return f 66 } 67 68 func (f *EnvAWSFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error { 69 cfg := request.Config 70 71 timeout := AwsMetadataTimeout 72 73 // Check if we should tighten the timeout 74 if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) { 75 timeout = 1 * time.Millisecond 76 } 77 78 ec2meta, err := ec2MetaClient(f.endpoint, timeout) 79 if err != nil { 80 return fmt.Errorf("failed to setup ec2Metadata client: %v", err) 81 } 82 83 if !isAWS(ec2meta) { 84 return nil 85 } 86 87 // Keys and whether they should be namespaced as unique. Any key whose value 88 // uniquely identifies a node, such as ip, should be marked as unique. When 89 // marked as unique, the key isn't included in the computed node class. 90 keys := map[string]bool{ 91 "ami-id": false, 92 "hostname": true, 93 "instance-id": true, 94 "instance-type": false, 95 "local-hostname": true, 96 "local-ipv4": true, 97 "public-hostname": true, 98 "public-ipv4": true, 99 "mac": true, 100 "placement/availability-zone": false, 101 } 102 103 for k, unique := range keys { 104 resp, err := ec2meta.GetMetadata(k) 105 v := strings.TrimSpace(resp) 106 if v == "" { 107 f.logger.Debug("read an empty value", "attribute", k) 108 continue 109 } else if awsErr, ok := err.(awserr.RequestFailure); ok { 110 f.logger.Debug("could not read attribute value", "attribute", k, "error", awsErr) 111 continue 112 } else if awsErr, ok := err.(awserr.Error); ok { 113 // if it's a URL error, assume we're not in an AWS environment 114 // TODO: better way to detect AWS? Check xen virtualization? 115 if _, ok := awsErr.OrigErr().(*url.Error); ok { 116 return nil 117 } 118 119 // not sure what other errors it would return 120 return err 121 } 122 123 // assume we want blank entries 124 key := "platform.aws." + strings.Replace(k, "/", ".", -1) 125 if unique { 126 key = structs.UniqueNamespace(key) 127 } 128 129 response.AddAttribute(key, v) 130 } 131 132 // accumulate resource information, then assign to response 133 var resources *structs.Resources 134 var nodeResources *structs.NodeResources 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 nodeResources = new(structs.NodeResources) 140 nodeResources.Networks = []*structs.NetworkResource{ 141 { 142 Mode: "host", 143 Device: "eth0", 144 IP: val, 145 CIDR: val + "/32", 146 MBits: f.throughput(request, ec2meta, val), 147 }, 148 } 149 } 150 151 // copy over IPv6 network specific information 152 if val, ok := response.Attributes["unique.platform.aws.mac"]; ok && val != "" { 153 k := "network/interfaces/macs/" + val + "/ipv6s" 154 addrsStr, err := ec2meta.GetMetadata(k) 155 addrsStr = strings.TrimSpace(addrsStr) 156 if addrsStr == "" { 157 f.logger.Debug("read an empty value", "attribute", k) 158 } else if awsErr, ok := err.(awserr.RequestFailure); ok { 159 f.logger.Debug("could not read attribute value", "attribute", k, "error", awsErr) 160 } else if awsErr, ok := err.(awserr.Error); ok { 161 // if it's a URL error, assume we're not in an AWS environment 162 // TODO: better way to detect AWS? Check xen virtualization? 163 if _, ok := awsErr.OrigErr().(*url.Error); ok { 164 return nil 165 } 166 167 // not sure what other errors it would return 168 return err 169 } else { 170 addrs := strings.SplitN(addrsStr, "\n", 2) 171 response.AddAttribute("unique.platform.aws.public-ipv6", addrs[0]) 172 } 173 } 174 175 // copy over CPU speed information 176 if specs := f.lookupCPU(ec2meta); specs != nil { 177 response.AddAttribute("cpu.frequency", fmt.Sprintf("%d", specs.MHz)) 178 response.AddAttribute("cpu.numcores", fmt.Sprintf("%d", specs.Cores)) 179 f.logger.Debug("lookup ec2 cpu", "cores", specs.Cores, "ghz", log.Fmt("%.1f", specs.GHz())) 180 181 if ticks := specs.Ticks(); request.Config.CpuCompute <= 0 { 182 response.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", ticks)) 183 f.logger.Debug("setting ec2 cpu", "ticks", ticks) 184 resources = new(structs.Resources) 185 resources.CPU = ticks 186 if nodeResources == nil { 187 nodeResources = new(structs.NodeResources) 188 } 189 nodeResources.Cpu = structs.NodeCpuResources{CpuShares: int64(ticks)} 190 } else { 191 response.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", request.Config.CpuCompute)) 192 } 193 } else { 194 f.logger.Warn("failed to find the cpu specification for this instance type") 195 } 196 197 response.Resources = resources 198 response.NodeResources = nodeResources 199 200 // populate Links 201 response.AddLink("aws.ec2", fmt.Sprintf("%s.%s", 202 response.Attributes["platform.aws.placement.availability-zone"], 203 response.Attributes["unique.platform.aws.instance-id"])) 204 response.Detected = true 205 206 return nil 207 } 208 209 func (f *EnvAWSFingerprint) instanceType(ec2meta *ec2metadata.EC2Metadata) (string, error) { 210 response, err := ec2meta.GetMetadata("instance-type") 211 if err != nil { 212 return "", err 213 } 214 return strings.TrimSpace(response), nil 215 } 216 217 func (f *EnvAWSFingerprint) lookupCPU(ec2meta *ec2metadata.EC2Metadata) *CPU { 218 instanceType, err := f.instanceType(ec2meta) 219 if err != nil { 220 f.logger.Warn("failed to read EC2 metadata instance-type", "error", err) 221 return nil 222 } 223 return LookupEC2CPU(instanceType) 224 } 225 226 func (f *EnvAWSFingerprint) throughput(request *FingerprintRequest, ec2meta *ec2metadata.EC2Metadata, ip string) int { 227 throughput := request.Config.NetworkSpeed 228 if throughput != 0 { 229 return throughput 230 } 231 232 throughput = f.linkSpeed(ec2meta) 233 if throughput != 0 { 234 return throughput 235 } 236 237 if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 { 238 for _, n := range request.Node.Resources.Networks { 239 if n.IP == ip { 240 return n.MBits 241 } 242 } 243 } 244 245 return defaultNetworkSpeed 246 } 247 248 // EnvAWSFingerprint uses lookup table to approximate network speeds 249 func (f *EnvAWSFingerprint) linkSpeed(ec2meta *ec2metadata.EC2Metadata) int { 250 instanceType, err := f.instanceType(ec2meta) 251 if err != nil { 252 f.logger.Error("error reading instance-type", "error", err) 253 return 0 254 } 255 256 netSpeed := 0 257 for reg, speed := range ec2NetSpeedTable { 258 if reg.MatchString(instanceType) { 259 netSpeed = speed 260 break 261 } 262 } 263 264 return netSpeed 265 } 266 267 func ec2MetaClient(endpoint string, timeout time.Duration) (*ec2metadata.EC2Metadata, error) { 268 client := &http.Client{ 269 Timeout: timeout, 270 Transport: cleanhttp.DefaultTransport(), 271 } 272 273 c := aws.NewConfig().WithHTTPClient(client).WithMaxRetries(0) 274 if endpoint != "" { 275 c = c.WithEndpoint(endpoint) 276 } 277 278 sess, err := session.NewSession(c) 279 if err != nil { 280 return nil, err 281 } 282 return ec2metadata.New(sess, c), nil 283 } 284 285 func isAWS(ec2meta *ec2metadata.EC2Metadata) bool { 286 v, err := ec2meta.GetMetadata("ami-id") 287 v = strings.TrimSpace(v) 288 return err == nil && v != "" 289 }