Home | History | Annotate | Download | only in metadata
      1 // Copyright 2014 Google Inc. All Rights Reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //      http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 // Package metadata provides access to Google Compute Engine (GCE)
     16 // metadata and API service accounts.
     17 //
     18 // This package is a wrapper around the GCE metadata service,
     19 // as documented at https://developers.google.com/compute/docs/metadata.
     20 package metadata
     21 
     22 import (
     23 	"encoding/json"
     24 	"fmt"
     25 	"io/ioutil"
     26 	"net"
     27 	"net/http"
     28 	"net/url"
     29 	"os"
     30 	"runtime"
     31 	"strings"
     32 	"sync"
     33 	"time"
     34 
     35 	"golang.org/x/net/context"
     36 	"golang.org/x/net/context/ctxhttp"
     37 )
     38 
     39 const (
     40 	// metadataIP is the documented metadata server IP address.
     41 	metadataIP = "169.254.169.254"
     42 
     43 	// metadataHostEnv is the environment variable specifying the
     44 	// GCE metadata hostname.  If empty, the default value of
     45 	// metadataIP ("169.254.169.254") is used instead.
     46 	// This is variable name is not defined by any spec, as far as
     47 	// I know; it was made up for the Go package.
     48 	metadataHostEnv = "GCE_METADATA_HOST"
     49 
     50 	userAgent = "gcloud-golang/0.1"
     51 )
     52 
     53 type cachedValue struct {
     54 	k    string
     55 	trim bool
     56 	mu   sync.Mutex
     57 	v    string
     58 }
     59 
     60 var (
     61 	projID  = &cachedValue{k: "project/project-id", trim: true}
     62 	projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
     63 	instID  = &cachedValue{k: "instance/id", trim: true}
     64 )
     65 
     66 var (
     67 	metaClient = &http.Client{
     68 		Transport: &http.Transport{
     69 			Dial: (&net.Dialer{
     70 				Timeout:   2 * time.Second,
     71 				KeepAlive: 30 * time.Second,
     72 			}).Dial,
     73 			ResponseHeaderTimeout: 2 * time.Second,
     74 		},
     75 	}
     76 	subscribeClient = &http.Client{
     77 		Transport: &http.Transport{
     78 			Dial: (&net.Dialer{
     79 				Timeout:   2 * time.Second,
     80 				KeepAlive: 30 * time.Second,
     81 			}).Dial,
     82 		},
     83 	}
     84 )
     85 
     86 // NotDefinedError is returned when requested metadata is not defined.
     87 //
     88 // The underlying string is the suffix after "/computeMetadata/v1/".
     89 //
     90 // This error is not returned if the value is defined to be the empty
     91 // string.
     92 type NotDefinedError string
     93 
     94 func (suffix NotDefinedError) Error() string {
     95 	return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
     96 }
     97 
     98 // Get returns a value from the metadata service.
     99 // The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
    100 //
    101 // If the GCE_METADATA_HOST environment variable is not defined, a default of
    102 // 169.254.169.254 will be used instead.
    103 //
    104 // If the requested metadata is not defined, the returned error will
    105 // be of type NotDefinedError.
    106 func Get(suffix string) (string, error) {
    107 	val, _, err := getETag(metaClient, suffix)
    108 	return val, err
    109 }
    110 
    111 // getETag returns a value from the metadata service as well as the associated
    112 // ETag using the provided client. This func is otherwise equivalent to Get.
    113 func getETag(client *http.Client, suffix string) (value, etag string, err error) {
    114 	// Using a fixed IP makes it very difficult to spoof the metadata service in
    115 	// a container, which is an important use-case for local testing of cloud
    116 	// deployments. To enable spoofing of the metadata service, the environment
    117 	// variable GCE_METADATA_HOST is first inspected to decide where metadata
    118 	// requests shall go.
    119 	host := os.Getenv(metadataHostEnv)
    120 	if host == "" {
    121 		// Using 169.254.169.254 instead of "metadata" here because Go
    122 		// binaries built with the "netgo" tag and without cgo won't
    123 		// know the search suffix for "metadata" is
    124 		// ".google.internal", and this IP address is documented as
    125 		// being stable anyway.
    126 		host = metadataIP
    127 	}
    128 	url := "http://" + host + "/computeMetadata/v1/" + suffix
    129 	req, _ := http.NewRequest("GET", url, nil)
    130 	req.Header.Set("Metadata-Flavor", "Google")
    131 	req.Header.Set("User-Agent", userAgent)
    132 	res, err := client.Do(req)
    133 	if err != nil {
    134 		return "", "", err
    135 	}
    136 	defer res.Body.Close()
    137 	if res.StatusCode == http.StatusNotFound {
    138 		return "", "", NotDefinedError(suffix)
    139 	}
    140 	if res.StatusCode != 200 {
    141 		return "", "", fmt.Errorf("status code %d trying to fetch %s", res.StatusCode, url)
    142 	}
    143 	all, err := ioutil.ReadAll(res.Body)
    144 	if err != nil {
    145 		return "", "", err
    146 	}
    147 	return string(all), res.Header.Get("Etag"), nil
    148 }
    149 
    150 func getTrimmed(suffix string) (s string, err error) {
    151 	s, err = Get(suffix)
    152 	s = strings.TrimSpace(s)
    153 	return
    154 }
    155 
    156 func (c *cachedValue) get() (v string, err error) {
    157 	defer c.mu.Unlock()
    158 	c.mu.Lock()
    159 	if c.v != "" {
    160 		return c.v, nil
    161 	}
    162 	if c.trim {
    163 		v, err = getTrimmed(c.k)
    164 	} else {
    165 		v, err = Get(c.k)
    166 	}
    167 	if err == nil {
    168 		c.v = v
    169 	}
    170 	return
    171 }
    172 
    173 var (
    174 	onGCEOnce sync.Once
    175 	onGCE     bool
    176 )
    177 
    178 // OnGCE reports whether this process is running on Google Compute Engine.
    179 func OnGCE() bool {
    180 	onGCEOnce.Do(initOnGCE)
    181 	return onGCE
    182 }
    183 
    184 func initOnGCE() {
    185 	onGCE = testOnGCE()
    186 }
    187 
    188 func testOnGCE() bool {
    189 	// The user explicitly said they're on GCE, so trust them.
    190 	if os.Getenv(metadataHostEnv) != "" {
    191 		return true
    192 	}
    193 
    194 	ctx, cancel := context.WithCancel(context.Background())
    195 	defer cancel()
    196 
    197 	resc := make(chan bool, 2)
    198 
    199 	// Try two strategies in parallel.
    200 	// See https://github.com/GoogleCloudPlatform/google-cloud-go/issues/194
    201 	go func() {
    202 		req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
    203 		req.Header.Set("User-Agent", userAgent)
    204 		res, err := ctxhttp.Do(ctx, metaClient, req)
    205 		if err != nil {
    206 			resc <- false
    207 			return
    208 		}
    209 		defer res.Body.Close()
    210 		resc <- res.Header.Get("Metadata-Flavor") == "Google"
    211 	}()
    212 
    213 	go func() {
    214 		addrs, err := net.LookupHost("metadata.google.internal")
    215 		if err != nil || len(addrs) == 0 {
    216 			resc <- false
    217 			return
    218 		}
    219 		resc <- strsContains(addrs, metadataIP)
    220 	}()
    221 
    222 	tryHarder := systemInfoSuggestsGCE()
    223 	if tryHarder {
    224 		res := <-resc
    225 		if res {
    226 			// The first strategy succeeded, so let's use it.
    227 			return true
    228 		}
    229 		// Wait for either the DNS or metadata server probe to
    230 		// contradict the other one and say we are running on
    231 		// GCE. Give it a lot of time to do so, since the system
    232 		// info already suggests we're running on a GCE BIOS.
    233 		timer := time.NewTimer(5 * time.Second)
    234 		defer timer.Stop()
    235 		select {
    236 		case res = <-resc:
    237 			return res
    238 		case <-timer.C:
    239 			// Too slow. Who knows what this system is.
    240 			return false
    241 		}
    242 	}
    243 
    244 	// There's no hint from the system info that we're running on
    245 	// GCE, so use the first probe's result as truth, whether it's
    246 	// true or false. The goal here is to optimize for speed for
    247 	// users who are NOT running on GCE. We can't assume that
    248 	// either a DNS lookup or an HTTP request to a blackholed IP
    249 	// address is fast. Worst case this should return when the
    250 	// metaClient's Transport.ResponseHeaderTimeout or
    251 	// Transport.Dial.Timeout fires (in two seconds).
    252 	return <-resc
    253 }
    254 
    255 // systemInfoSuggestsGCE reports whether the local system (without
    256 // doing network requests) suggests that we're running on GCE. If this
    257 // returns true, testOnGCE tries a bit harder to reach its metadata
    258 // server.
    259 func systemInfoSuggestsGCE() bool {
    260 	if runtime.GOOS != "linux" {
    261 		// We don't have any non-Linux clues available, at least yet.
    262 		return false
    263 	}
    264 	slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
    265 	name := strings.TrimSpace(string(slurp))
    266 	return name == "Google" || name == "Google Compute Engine"
    267 }
    268 
    269 // Subscribe subscribes to a value from the metadata service.
    270 // The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
    271 // The suffix may contain query parameters.
    272 //
    273 // Subscribe calls fn with the latest metadata value indicated by the provided
    274 // suffix. If the metadata value is deleted, fn is called with the empty string
    275 // and ok false. Subscribe blocks until fn returns a non-nil error or the value
    276 // is deleted. Subscribe returns the error value returned from the last call to
    277 // fn, which may be nil when ok == false.
    278 func Subscribe(suffix string, fn func(v string, ok bool) error) error {
    279 	const failedSubscribeSleep = time.Second * 5
    280 
    281 	// First check to see if the metadata value exists at all.
    282 	val, lastETag, err := getETag(subscribeClient, suffix)
    283 	if err != nil {
    284 		return err
    285 	}
    286 
    287 	if err := fn(val, true); err != nil {
    288 		return err
    289 	}
    290 
    291 	ok := true
    292 	if strings.ContainsRune(suffix, '?') {
    293 		suffix += "&wait_for_change=true&last_etag="
    294 	} else {
    295 		suffix += "?wait_for_change=true&last_etag="
    296 	}
    297 	for {
    298 		val, etag, err := getETag(subscribeClient, suffix+url.QueryEscape(lastETag))
    299 		if err != nil {
    300 			if _, deleted := err.(NotDefinedError); !deleted {
    301 				time.Sleep(failedSubscribeSleep)
    302 				continue // Retry on other errors.
    303 			}
    304 			ok = false
    305 		}
    306 		lastETag = etag
    307 
    308 		if err := fn(val, ok); err != nil || !ok {
    309 			return err
    310 		}
    311 	}
    312 }
    313 
    314 // ProjectID returns the current instance's project ID string.
    315 func ProjectID() (string, error) { return projID.get() }
    316 
    317 // NumericProjectID returns the current instance's numeric project ID.
    318 func NumericProjectID() (string, error) { return projNum.get() }
    319 
    320 // InternalIP returns the instance's primary internal IP address.
    321 func InternalIP() (string, error) {
    322 	return getTrimmed("instance/network-interfaces/0/ip")
    323 }
    324 
    325 // ExternalIP returns the instance's primary external (public) IP address.
    326 func ExternalIP() (string, error) {
    327 	return getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
    328 }
    329 
    330 // Hostname returns the instance's hostname. This will be of the form
    331 // "<instanceID>.c.<projID>.internal".
    332 func Hostname() (string, error) {
    333 	return getTrimmed("instance/hostname")
    334 }
    335 
    336 // InstanceTags returns the list of user-defined instance tags,
    337 // assigned when initially creating a GCE instance.
    338 func InstanceTags() ([]string, error) {
    339 	var s []string
    340 	j, err := Get("instance/tags")
    341 	if err != nil {
    342 		return nil, err
    343 	}
    344 	if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
    345 		return nil, err
    346 	}
    347 	return s, nil
    348 }
    349 
    350 // InstanceID returns the current VM's numeric instance ID.
    351 func InstanceID() (string, error) {
    352 	return instID.get()
    353 }
    354 
    355 // InstanceName returns the current VM's instance ID string.
    356 func InstanceName() (string, error) {
    357 	host, err := Hostname()
    358 	if err != nil {
    359 		return "", err
    360 	}
    361 	return strings.Split(host, ".")[0], nil
    362 }
    363 
    364 // Zone returns the current VM's zone, such as "us-central1-b".
    365 func Zone() (string, error) {
    366 	zone, err := getTrimmed("instance/zone")
    367 	// zone is of the form "projects/<projNum>/zones/<zoneName>".
    368 	if err != nil {
    369 		return "", err
    370 	}
    371 	return zone[strings.LastIndex(zone, "/")+1:], nil
    372 }
    373 
    374 // InstanceAttributes returns the list of user-defined attributes,
    375 // assigned when initially creating a GCE VM instance. The value of an
    376 // attribute can be obtained with InstanceAttributeValue.
    377 func InstanceAttributes() ([]string, error) { return lines("instance/attributes/") }
    378 
    379 // ProjectAttributes returns the list of user-defined attributes
    380 // applying to the project as a whole, not just this VM.  The value of
    381 // an attribute can be obtained with ProjectAttributeValue.
    382 func ProjectAttributes() ([]string, error) { return lines("project/attributes/") }
    383 
    384 func lines(suffix string) ([]string, error) {
    385 	j, err := Get(suffix)
    386 	if err != nil {
    387 		return nil, err
    388 	}
    389 	s := strings.Split(strings.TrimSpace(j), "\n")
    390 	for i := range s {
    391 		s[i] = strings.TrimSpace(s[i])
    392 	}
    393 	return s, nil
    394 }
    395 
    396 // InstanceAttributeValue returns the value of the provided VM
    397 // instance attribute.
    398 //
    399 // If the requested attribute is not defined, the returned error will
    400 // be of type NotDefinedError.
    401 //
    402 // InstanceAttributeValue may return ("", nil) if the attribute was
    403 // defined to be the empty string.
    404 func InstanceAttributeValue(attr string) (string, error) {
    405 	return Get("instance/attributes/" + attr)
    406 }
    407 
    408 // ProjectAttributeValue returns the value of the provided
    409 // project attribute.
    410 //
    411 // If the requested attribute is not defined, the returned error will
    412 // be of type NotDefinedError.
    413 //
    414 // ProjectAttributeValue may return ("", nil) if the attribute was
    415 // defined to be the empty string.
    416 func ProjectAttributeValue(attr string) (string, error) {
    417 	return Get("project/attributes/" + attr)
    418 }
    419 
    420 // Scopes returns the service account scopes for the given account.
    421 // The account may be empty or the string "default" to use the instance's
    422 // main account.
    423 func Scopes(serviceAccount string) ([]string, error) {
    424 	if serviceAccount == "" {
    425 		serviceAccount = "default"
    426 	}
    427 	return lines("instance/service-accounts/" + serviceAccount + "/scopes")
    428 }
    429 
    430 func strsContains(ss []string, s string) bool {
    431 	for _, v := range ss {
    432 		if v == s {
    433 			return true
    434 		}
    435 	}
    436 	return false
    437 }
    438