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. 20package metadata 21 22import ( 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 39const ( 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 53type cachedValue struct { 54 k string 55 trim bool 56 mu sync.Mutex 57 v string 58} 59 60var ( 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 66var ( 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. 92type NotDefinedError string 93 94func (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. 106func 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. 113func 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 150func getTrimmed(suffix string) (s string, err error) { 151 s, err = Get(suffix) 152 s = strings.TrimSpace(s) 153 return 154} 155 156func (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 173var ( 174 onGCEOnce sync.Once 175 onGCE bool 176) 177 178// OnGCE reports whether this process is running on Google Compute Engine. 179func OnGCE() bool { 180 onGCEOnce.Do(initOnGCE) 181 return onGCE 182} 183 184func initOnGCE() { 185 onGCE = testOnGCE() 186} 187 188func 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. 259func 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. 278func 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. 315func ProjectID() (string, error) { return projID.get() } 316 317// NumericProjectID returns the current instance's numeric project ID. 318func NumericProjectID() (string, error) { return projNum.get() } 319 320// InternalIP returns the instance's primary internal IP address. 321func InternalIP() (string, error) { 322 return getTrimmed("instance/network-interfaces/0/ip") 323} 324 325// ExternalIP returns the instance's primary external (public) IP address. 326func 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". 332func 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. 338func 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. 351func InstanceID() (string, error) { 352 return instID.get() 353} 354 355// InstanceName returns the current VM's instance ID string. 356func 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". 365func 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. 377func 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. 382func ProjectAttributes() ([]string, error) { return lines("project/attributes/") } 383 384func 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. 404func 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. 416func 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. 423func Scopes(serviceAccount string) ([]string, error) { 424 if serviceAccount == "" { 425 serviceAccount = "default" 426 } 427 return lines("instance/service-accounts/" + serviceAccount + "/scopes") 428} 429 430func 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