github.com/smithx10/nomad@v0.9.1-rc1/client/fingerprint/env_gce.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    16  	log "github.com/hashicorp/go-hclog"
    17  
    18  	"github.com/hashicorp/nomad/helper/useragent"
    19  	"github.com/hashicorp/nomad/nomad/structs"
    20  )
    21  
    22  const (
    23  	// This is where the GCE metadata server normally resides. We hardcode the
    24  	// "instance" path as well since it's the only one we access here.
    25  	DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/"
    26  
    27  	// GceMetadataTimeout is the timeout used when contacting the GCE metadata
    28  	// service
    29  	GceMetadataTimeout = 2 * time.Second
    30  )
    31  
    32  type GCEMetadataNetworkInterface struct {
    33  	AccessConfigs []struct {
    34  		ExternalIp string
    35  		Type       string
    36  	}
    37  	ForwardedIps []string
    38  	Ip           string
    39  	Network      string
    40  }
    41  
    42  type ReqError struct {
    43  	StatusCode int
    44  }
    45  
    46  func (e ReqError) Error() string {
    47  	return http.StatusText(e.StatusCode)
    48  }
    49  
    50  func lastToken(s string) string {
    51  	index := strings.LastIndex(s, "/")
    52  	return s[index+1:]
    53  }
    54  
    55  // EnvGCEFingerprint is used to fingerprint GCE metadata
    56  type EnvGCEFingerprint struct {
    57  	StaticFingerprinter
    58  	client      *http.Client
    59  	logger      log.Logger
    60  	metadataURL string
    61  }
    62  
    63  // NewEnvGCEFingerprint is used to create a fingerprint from GCE metadata
    64  func NewEnvGCEFingerprint(logger log.Logger) Fingerprint {
    65  	// Read the internal metadata URL from the environment, allowing test files to
    66  	// provide their own
    67  	metadataURL := os.Getenv("GCE_ENV_URL")
    68  	if metadataURL == "" {
    69  		metadataURL = DEFAULT_GCE_URL
    70  	}
    71  
    72  	// assume 2 seconds is enough time for inside GCE network
    73  	client := &http.Client{
    74  		Timeout:   GceMetadataTimeout,
    75  		Transport: cleanhttp.DefaultTransport(),
    76  	}
    77  
    78  	return &EnvGCEFingerprint{
    79  		client:      client,
    80  		logger:      logger.Named("env_gce"),
    81  		metadataURL: metadataURL,
    82  	}
    83  }
    84  
    85  func (f *EnvGCEFingerprint) Get(attribute string, recursive bool) (string, error) {
    86  	reqUrl := f.metadataURL + attribute
    87  	if recursive {
    88  		reqUrl = reqUrl + "?recursive=true"
    89  	}
    90  
    91  	parsedUrl, err := url.Parse(reqUrl)
    92  	if err != nil {
    93  		return "", err
    94  	}
    95  
    96  	req := &http.Request{
    97  		Method: "GET",
    98  		URL:    parsedUrl,
    99  		Header: http.Header{
   100  			"Metadata-Flavor": []string{"Google"},
   101  			"User-Agent":      []string{useragent.String()},
   102  		},
   103  	}
   104  
   105  	res, err := f.client.Do(req)
   106  	if err != nil {
   107  		f.logger.Debug("could not read value for attribute", "attribute", attribute, "error", err)
   108  		return "", err
   109  	} else if res.StatusCode != http.StatusOK {
   110  		f.logger.Debug("could not read value for attribute", "attribute", attribute, "resp_code", res.StatusCode)
   111  		return "", err
   112  	}
   113  
   114  	resp, err := ioutil.ReadAll(res.Body)
   115  	res.Body.Close()
   116  	if err != nil {
   117  		f.logger.Error("error reading response body for GCE attribute", "attribute", attribute, "error", err)
   118  		return "", err
   119  	}
   120  
   121  	if res.StatusCode >= 400 {
   122  		return "", ReqError{res.StatusCode}
   123  	}
   124  
   125  	return string(resp), nil
   126  }
   127  
   128  func checkError(err error, logger log.Logger, desc string) error {
   129  	// If it's a URL error, assume we're not actually in an GCE environment.
   130  	// To the outer layers, this isn't an error so return nil.
   131  	if _, ok := err.(*url.Error); ok {
   132  		logger.Debug("error querying GCE attribute; skipping", "attribute", desc)
   133  		return nil
   134  	}
   135  	// Otherwise pass the error through.
   136  	return err
   137  }
   138  
   139  func (f *EnvGCEFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error {
   140  	cfg := req.Config
   141  
   142  	// Check if we should tighten the timeout
   143  	if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
   144  		f.client.Timeout = 1 * time.Millisecond
   145  	}
   146  
   147  	if !f.isGCE() {
   148  		return nil
   149  	}
   150  
   151  	// Keys and whether they should be namespaced as unique. Any key whose value
   152  	// uniquely identifies a node, such as ip, should be marked as unique. When
   153  	// marked as unique, the key isn't included in the computed node class.
   154  	keys := map[string]bool{
   155  		"hostname":                       true,
   156  		"id":                             true,
   157  		"cpu-platform":                   false,
   158  		"scheduling/automatic-restart":   false,
   159  		"scheduling/on-host-maintenance": false,
   160  	}
   161  
   162  	for k, unique := range keys {
   163  		value, err := f.Get(k, false)
   164  		if err != nil {
   165  			return checkError(err, f.logger, k)
   166  		}
   167  
   168  		// assume we want blank entries
   169  		key := "platform.gce." + strings.Replace(k, "/", ".", -1)
   170  		if unique {
   171  			key = structs.UniqueNamespace(key)
   172  		}
   173  		resp.AddAttribute(key, strings.Trim(value, "\n"))
   174  	}
   175  
   176  	// These keys need everything before the final slash removed to be usable.
   177  	keys = map[string]bool{
   178  		"machine-type": false,
   179  		"zone":         false,
   180  	}
   181  	for k, unique := range keys {
   182  		value, err := f.Get(k, false)
   183  		if err != nil {
   184  			return checkError(err, f.logger, k)
   185  		}
   186  
   187  		key := "platform.gce." + k
   188  		if unique {
   189  			key = structs.UniqueNamespace(key)
   190  		}
   191  		resp.AddAttribute(key, strings.Trim(lastToken(value), "\n"))
   192  	}
   193  
   194  	// Get internal and external IPs (if they exist)
   195  	value, err := f.Get("network-interfaces/", true)
   196  	if err != nil {
   197  		f.logger.Warn("error retrieving network interface information", "error", err)
   198  	} else {
   199  
   200  		var interfaces []GCEMetadataNetworkInterface
   201  		if err := json.Unmarshal([]byte(value), &interfaces); err != nil {
   202  			f.logger.Warn("error decoding network interface information", "error", err)
   203  		}
   204  
   205  		for _, intf := range interfaces {
   206  			prefix := "platform.gce.network." + lastToken(intf.Network)
   207  			uniquePrefix := "unique." + prefix
   208  			resp.AddAttribute(prefix, "true")
   209  			resp.AddAttribute(uniquePrefix+".ip", strings.Trim(intf.Ip, "\n"))
   210  			for index, accessConfig := range intf.AccessConfigs {
   211  				resp.AddAttribute(uniquePrefix+".external-ip."+strconv.Itoa(index), accessConfig.ExternalIp)
   212  			}
   213  		}
   214  	}
   215  
   216  	var tagList []string
   217  	value, err = f.Get("tags", false)
   218  	if err != nil {
   219  		return checkError(err, f.logger, "tags")
   220  	}
   221  	if err := json.Unmarshal([]byte(value), &tagList); err != nil {
   222  		f.logger.Warn("error decoding instance tags", "error", err)
   223  	}
   224  	for _, tag := range tagList {
   225  		attr := "platform.gce.tag."
   226  		var key string
   227  
   228  		// If the tag is namespaced as unique, we strip it from the tag and
   229  		// prepend to the whole attribute.
   230  		if structs.IsUniqueNamespace(tag) {
   231  			tag = strings.TrimPrefix(tag, structs.NodeUniqueNamespace)
   232  			key = fmt.Sprintf("%s%s%s", structs.NodeUniqueNamespace, attr, tag)
   233  		} else {
   234  			key = fmt.Sprintf("%s%s", attr, tag)
   235  		}
   236  
   237  		resp.AddAttribute(key, "true")
   238  	}
   239  
   240  	var attrDict map[string]string
   241  	value, err = f.Get("attributes/", true)
   242  	if err != nil {
   243  		return checkError(err, f.logger, "attributes/")
   244  	}
   245  	if err := json.Unmarshal([]byte(value), &attrDict); err != nil {
   246  		f.logger.Warn("error decoding instance attributes", "error", err)
   247  	}
   248  	for k, v := range attrDict {
   249  		attr := "platform.gce.attr."
   250  		var key string
   251  
   252  		// If the key is namespaced as unique, we strip it from the
   253  		// key and prepend to the whole attribute.
   254  		if structs.IsUniqueNamespace(k) {
   255  			k = strings.TrimPrefix(k, structs.NodeUniqueNamespace)
   256  			key = fmt.Sprintf("%s%s%s", structs.NodeUniqueNamespace, attr, k)
   257  		} else {
   258  			key = fmt.Sprintf("%s%s", attr, k)
   259  		}
   260  
   261  		resp.AddAttribute(key, strings.Trim(v, "\n"))
   262  	}
   263  
   264  	// populate Links
   265  	if id, ok := resp.Attributes["unique.platform.gce.id"]; ok {
   266  		resp.AddLink("gce", id)
   267  	}
   268  
   269  	resp.Detected = true
   270  
   271  	return nil
   272  }
   273  
   274  func (f *EnvGCEFingerprint) isGCE() bool {
   275  	// TODO: better way to detect GCE?
   276  
   277  	// Query the metadata url for the machine type, to verify we're on GCE
   278  	machineType, err := f.Get("machine-type", false)
   279  	if err != nil {
   280  		if re, ok := err.(ReqError); !ok || re.StatusCode != 404 {
   281  			// If it wasn't a 404 error, print an error message.
   282  			f.logger.Debug("error querying GCE Metadata URL, skipping")
   283  		}
   284  		return false
   285  	}
   286  
   287  	match, err := regexp.MatchString("projects/.+/machineTypes/.+", machineType)
   288  	if err != nil || !match {
   289  		return false
   290  	}
   291  
   292  	return true
   293  }