github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/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-life-cycle": false, 95 "instance-type": false, 96 "local-hostname": true, 97 "local-ipv4": true, 98 "public-hostname": true, 99 "public-ipv4": true, 100 "mac": true, 101 "placement/availability-zone": false, 102 } 103 104 for k, unique := range keys { 105 resp, err := ec2meta.GetMetadata(k) 106 v := strings.TrimSpace(resp) 107 if v == "" { 108 f.logger.Debug("read an empty value", "attribute", k) 109 continue 110 } else if awsErr, ok := err.(awserr.RequestFailure); ok { 111 f.logger.Debug("could not read attribute value", "attribute", k, "error", awsErr) 112 continue 113 } else if awsErr, ok := err.(awserr.Error); ok { 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 := awsErr.OrigErr().(*url.Error); ok { 117 return nil 118 } 119 120 // not sure what other errors it would return 121 return err 122 } 123 124 // assume we want blank entries 125 key := "platform.aws." + strings.ReplaceAll(k, "/", ".") 126 if unique { 127 key = structs.UniqueNamespace(key) 128 } 129 130 response.AddAttribute(key, v) 131 } 132 133 // accumulate resource information, then assign to response 134 var resources *structs.Resources 135 var nodeResources *structs.NodeResources 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 nodeResources = new(structs.NodeResources) 141 nodeResources.Networks = []*structs.NetworkResource{ 142 { 143 Mode: "host", 144 Device: "eth0", 145 IP: val, 146 CIDR: val + "/32", 147 MBits: f.throughput(request, ec2meta, val), 148 }, 149 } 150 } 151 152 // copy over IPv6 network specific information 153 if val, ok := response.Attributes["unique.platform.aws.mac"]; ok && val != "" { 154 k := "network/interfaces/macs/" + val + "/ipv6s" 155 addrsStr, err := ec2meta.GetMetadata(k) 156 addrsStr = strings.TrimSpace(addrsStr) 157 if addrsStr == "" { 158 f.logger.Debug("read an empty value", "attribute", k) 159 } else if awsErr, ok := err.(awserr.RequestFailure); ok { 160 f.logger.Debug("could not read attribute value", "attribute", k, "error", awsErr) 161 } else if awsErr, ok := err.(awserr.Error); ok { 162 // if it's a URL error, assume we're not in an AWS environment 163 // TODO: better way to detect AWS? Check xen virtualization? 164 if _, ok := awsErr.OrigErr().(*url.Error); ok { 165 return nil 166 } 167 168 // not sure what other errors it would return 169 return err 170 } else { 171 addrs := strings.SplitN(addrsStr, "\n", 2) 172 response.AddAttribute("unique.platform.aws.public-ipv6", addrs[0]) 173 } 174 } 175 176 // copy over CPU speed information 177 if specs := f.lookupCPU(ec2meta); specs != nil { 178 response.AddAttribute("cpu.frequency", fmt.Sprintf("%d", specs.MHz)) 179 response.AddAttribute("cpu.numcores", fmt.Sprintf("%d", specs.Cores)) 180 f.logger.Debug("lookup ec2 cpu", "cores", specs.Cores, "ghz", log.Fmt("%.1f", specs.GHz())) 181 182 if ticks := specs.Ticks(); request.Config.CpuCompute <= 0 { 183 response.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", ticks)) 184 f.logger.Debug("setting ec2 cpu", "ticks", ticks) 185 resources = new(structs.Resources) 186 resources.CPU = ticks 187 if nodeResources == nil { 188 nodeResources = new(structs.NodeResources) 189 } 190 nodeResources.Cpu = structs.NodeCpuResources{CpuShares: int64(ticks)} 191 } else { 192 response.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", request.Config.CpuCompute)) 193 } 194 } else { 195 f.logger.Warn("failed to find the cpu specification for this instance type") 196 } 197 198 response.Resources = resources 199 response.NodeResources = nodeResources 200 201 // populate Links 202 response.AddLink("aws.ec2", fmt.Sprintf("%s.%s", 203 response.Attributes["platform.aws.placement.availability-zone"], 204 response.Attributes["unique.platform.aws.instance-id"])) 205 response.Detected = true 206 207 return nil 208 } 209 210 func (f *EnvAWSFingerprint) instanceType(ec2meta *ec2metadata.EC2Metadata) (string, error) { 211 response, err := ec2meta.GetMetadata("instance-type") 212 if err != nil { 213 return "", err 214 } 215 return strings.TrimSpace(response), nil 216 } 217 218 func (f *EnvAWSFingerprint) lookupCPU(ec2meta *ec2metadata.EC2Metadata) *CPU { 219 instanceType, err := f.instanceType(ec2meta) 220 if err != nil { 221 f.logger.Warn("failed to read EC2 metadata instance-type", "error", err) 222 return nil 223 } 224 return LookupEC2CPU(instanceType) 225 } 226 227 func (f *EnvAWSFingerprint) throughput(request *FingerprintRequest, ec2meta *ec2metadata.EC2Metadata, ip string) int { 228 throughput := request.Config.NetworkSpeed 229 if throughput != 0 { 230 return throughput 231 } 232 233 throughput = f.linkSpeed(ec2meta) 234 if throughput != 0 { 235 return throughput 236 } 237 238 if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 { 239 for _, n := range request.Node.Resources.Networks { 240 if n.IP == ip { 241 return n.MBits 242 } 243 } 244 } 245 246 return defaultNetworkSpeed 247 } 248 249 // EnvAWSFingerprint uses lookup table to approximate network speeds 250 func (f *EnvAWSFingerprint) linkSpeed(ec2meta *ec2metadata.EC2Metadata) int { 251 instanceType, err := f.instanceType(ec2meta) 252 if err != nil { 253 f.logger.Error("error reading instance-type", "error", err) 254 return 0 255 } 256 257 netSpeed := 0 258 for reg, speed := range ec2NetSpeedTable { 259 if reg.MatchString(instanceType) { 260 netSpeed = speed 261 break 262 } 263 } 264 265 return netSpeed 266 } 267 268 func ec2MetaClient(endpoint string, timeout time.Duration) (*ec2metadata.EC2Metadata, error) { 269 client := &http.Client{ 270 Timeout: timeout, 271 Transport: cleanhttp.DefaultTransport(), 272 } 273 274 c := aws.NewConfig().WithHTTPClient(client).WithMaxRetries(0) 275 if endpoint != "" { 276 c = c.WithEndpoint(endpoint) 277 } 278 279 sess, err := session.NewSession(c) 280 if err != nil { 281 return nil, err 282 } 283 return ec2metadata.New(sess, c), nil 284 } 285 286 func isAWS(ec2meta *ec2metadata.EC2Metadata) bool { 287 v, err := ec2meta.GetMetadata("ami-id") 288 v = strings.TrimSpace(v) 289 return err == nil && v != "" 290 }