github.com/quite/nomad@v0.8.6/client/fingerprint/env_gce.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/hashicorp/go-cleanhttp"
    17  	cstructs "github.com/hashicorp/nomad/client/structs"
    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,
    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 || res.StatusCode != http.StatusOK {
   107  		f.logger.Printf("[DEBUG] fingerprint.env_gce: Could not read value for attribute %q", attribute)
   108  		return "", err
   109  	}
   110  
   111  	resp, err := ioutil.ReadAll(res.Body)
   112  	res.Body.Close()
   113  	if err != nil {
   114  		f.logger.Printf("[ERR] fingerprint.env_gce: Error reading response body for GCE %s", attribute)
   115  		return "", err
   116  	}
   117  
   118  	if res.StatusCode >= 400 {
   119  		return "", ReqError{res.StatusCode}
   120  	}
   121  
   122  	return string(resp), nil
   123  }
   124  
   125  func checkError(err error, logger *log.Logger, desc string) error {
   126  	// If it's a URL error, assume we're not actually in an GCE environment.
   127  	// To the outer layers, this isn't an error so return nil.
   128  	if _, ok := err.(*url.Error); ok {
   129  		logger.Printf("[DEBUG] fingerprint.env_gce: Error querying GCE " + desc + ", skipping")
   130  		return nil
   131  	}
   132  	// Otherwise pass the error through.
   133  	return err
   134  }
   135  
   136  func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error {
   137  	cfg := req.Config
   138  
   139  	// Check if we should tighten the timeout
   140  	if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
   141  		f.client.Timeout = 1 * time.Millisecond
   142  	}
   143  
   144  	if !f.isGCE() {
   145  		return nil
   146  	}
   147  
   148  	// Keys and whether they should be namespaced as unique. Any key whose value
   149  	// uniquely identifies a node, such as ip, should be marked as unique. When
   150  	// marked as unique, the key isn't included in the computed node class.
   151  	keys := map[string]bool{
   152  		"hostname":                       true,
   153  		"id":                             true,
   154  		"cpu-platform":                   false,
   155  		"scheduling/automatic-restart":   false,
   156  		"scheduling/on-host-maintenance": false,
   157  	}
   158  
   159  	for k, unique := range keys {
   160  		value, err := f.Get(k, false)
   161  		if err != nil {
   162  			return checkError(err, f.logger, k)
   163  		}
   164  
   165  		// assume we want blank entries
   166  		key := "platform.gce." + strings.Replace(k, "/", ".", -1)
   167  		if unique {
   168  			key = structs.UniqueNamespace(key)
   169  		}
   170  		resp.AddAttribute(key, strings.Trim(value, "\n"))
   171  	}
   172  
   173  	// These keys need everything before the final slash removed to be usable.
   174  	keys = map[string]bool{
   175  		"machine-type": false,
   176  		"zone":         false,
   177  	}
   178  	for k, unique := range keys {
   179  		value, err := f.Get(k, false)
   180  		if err != nil {
   181  			return checkError(err, f.logger, k)
   182  		}
   183  
   184  		key := "platform.gce." + k
   185  		if unique {
   186  			key = structs.UniqueNamespace(key)
   187  		}
   188  		resp.AddAttribute(key, strings.Trim(lastToken(value), "\n"))
   189  	}
   190  
   191  	// Get internal and external IPs (if they exist)
   192  	value, err := f.Get("network-interfaces/", true)
   193  	if err != nil {
   194  		f.logger.Printf("[WARN] fingerprint.env_gce: Error retrieving network interface information: %s", err)
   195  	} else {
   196  
   197  		var interfaces []GCEMetadataNetworkInterface
   198  		if err := json.Unmarshal([]byte(value), &interfaces); err != nil {
   199  			f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error())
   200  		}
   201  
   202  		for _, intf := range interfaces {
   203  			prefix := "platform.gce.network." + lastToken(intf.Network)
   204  			uniquePrefix := "unique." + prefix
   205  			resp.AddAttribute(prefix, "true")
   206  			resp.AddAttribute(uniquePrefix+".ip", strings.Trim(intf.Ip, "\n"))
   207  			for index, accessConfig := range intf.AccessConfigs {
   208  				resp.AddAttribute(uniquePrefix+".external-ip."+strconv.Itoa(index), accessConfig.ExternalIp)
   209  			}
   210  		}
   211  	}
   212  
   213  	var tagList []string
   214  	value, err = f.Get("tags", false)
   215  	if err != nil {
   216  		return checkError(err, f.logger, "tags")
   217  	}
   218  	if err := json.Unmarshal([]byte(value), &tagList); err != nil {
   219  		f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error())
   220  	}
   221  	for _, tag := range tagList {
   222  		attr := "platform.gce.tag."
   223  		var key string
   224  
   225  		// If the tag is namespaced as unique, we strip it from the tag and
   226  		// prepend to the whole attribute.
   227  		if structs.IsUniqueNamespace(tag) {
   228  			tag = strings.TrimPrefix(tag, structs.NodeUniqueNamespace)
   229  			key = fmt.Sprintf("%s%s%s", structs.NodeUniqueNamespace, attr, tag)
   230  		} else {
   231  			key = fmt.Sprintf("%s%s", attr, tag)
   232  		}
   233  
   234  		resp.AddAttribute(key, "true")
   235  	}
   236  
   237  	var attrDict map[string]string
   238  	value, err = f.Get("attributes/", true)
   239  	if err != nil {
   240  		return checkError(err, f.logger, "attributes/")
   241  	}
   242  	if err := json.Unmarshal([]byte(value), &attrDict); err != nil {
   243  		f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error())
   244  	}
   245  	for k, v := range attrDict {
   246  		attr := "platform.gce.attr."
   247  		var key string
   248  
   249  		// If the key is namespaced as unique, we strip it from the
   250  		// key and prepend to the whole attribute.
   251  		if structs.IsUniqueNamespace(k) {
   252  			k = strings.TrimPrefix(k, structs.NodeUniqueNamespace)
   253  			key = fmt.Sprintf("%s%s%s", structs.NodeUniqueNamespace, attr, k)
   254  		} else {
   255  			key = fmt.Sprintf("%s%s", attr, k)
   256  		}
   257  
   258  		resp.AddAttribute(key, strings.Trim(v, "\n"))
   259  	}
   260  
   261  	// populate Links
   262  	if id, ok := resp.Attributes["unique.platform.gce.id"]; ok {
   263  		resp.AddLink("gce", id)
   264  	}
   265  
   266  	resp.Detected = true
   267  
   268  	return nil
   269  }
   270  
   271  func (f *EnvGCEFingerprint) isGCE() bool {
   272  	// TODO: better way to detect GCE?
   273  
   274  	// Query the metadata url for the machine type, to verify we're on GCE
   275  	machineType, err := f.Get("machine-type", false)
   276  	if err != nil {
   277  		if re, ok := err.(ReqError); !ok || re.StatusCode != 404 {
   278  			// If it wasn't a 404 error, print an error message.
   279  			f.logger.Printf("[DEBUG] fingerprint.env_gce: Error querying GCE Metadata URL, skipping")
   280  		}
   281  		return false
   282  	}
   283  
   284  	match, err := regexp.MatchString("projects/.+/machineTypes/.+", machineType)
   285  	if err != nil || !match {
   286  		return false
   287  	}
   288  
   289  	return true
   290  }