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  }