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  }