Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GCE fingerprinting functionality #215

Merged
merged 13 commits into from
Oct 12, 2015
5 changes: 5 additions & 0 deletions client/fingerprint/env_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ func isAWS() bool {
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
// URL not found, which indicates that this isn't AWS
return false
}

instanceID, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("[ERR] fingerprint.env_aws: Error reading AWS Instance ID, skipping")
Expand Down
236 changes: 236 additions & 0 deletions client/fingerprint/env_gce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package fingerprint

import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"

"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/nomad/structs"
)

type GCEMetadataClient struct {
*http.Client
logger *log.Logger
metadataURL string
}

type ReqError struct {
StatusCode int
}

func (e ReqError) Error() string {
return http.StatusText(e.StatusCode)
}

func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient {
// Read the internal metadata URL from the environment, allowing test files to
// provide their own
metadataURL := os.Getenv("GCE_ENV_URL")
if metadataURL == "" {
metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this string to a const at the top and put a comment on it (make it look less like a magic value)

}

// assume 2 seconds is enough time for inside GCE network
client := &http.Client{
Timeout: 2 * time.Second,
}

return GCEMetadataClient{
Client: client,
logger: logger,
metadataURL: metadataURL,
}
}

func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) {
reqUrl := g.metadataURL + attribute
if recursive {
reqUrl = reqUrl + "?recursive=true"
}

parsedUrl, err := url.Parse(reqUrl)
if err != nil {
return "", err
}

req := &http.Request{
Method: "GET",
URL: parsedUrl,
Header: http.Header{
"Metadata-Flavor": []string{"Google"},
},
}

res, err := g.Client.Do(req)
if err != nil {
return "", err
}

resp, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
g.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute)
return "", err
}

if res.StatusCode >= 400 {
return "", ReqError{res.StatusCode}
}

return string(resp), nil
}

// EnvGCEFingerprint is used to fingerprint the CPU
type EnvGCEFingerprint struct {
logger *log.Logger
}

// NewEnvGCEFingerprint is used to create a CPU fingerprint
func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint {
f := &EnvGCEFingerprint{logger: logger}
return f
}

func checkError(err error, logger *log.Logger, desc string) error {
// If it's a URL error, assume we're not actually in an GCE environment.
// To the outer layers, this isn't an error so return nil.
if _, ok := err.(*url.Error); ok {
logger.Printf("[ERR] fingerprint.env_gce: Error querying GCE " + desc + ", skipping")
return nil
}
// Otherwise pass the error through.
return err
}

func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
if !f.isGCE() {
return false, nil
}

// newNetwork is populated and addded to the Nodes resources
newNetwork := &structs.NetworkResource{
Device: "eth0",
}

if node.Links == nil {
node.Links = make(map[string]string)
}

client := NewGCEMetadataClient(f.logger)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put the client in the EnvGCEFingerprint struct so it doesn't have to be made twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I originally did it that way so that the client wouldn't stay around forever once it was no longer needed, but I see that the Fingerprint objects aren't persistent anyway.


keys := []string{
"hostname",
"id",
}
for _, k := range keys {
value, err := client.Get(k, false)
if err != nil {
return false, checkError(err, f.logger, k)
}

// assume we want blank entries
node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n")
}

// These keys need everything before the final slash removed to be usable.
keys = []string{
"machine-type",
"zone",
}
for _, k := range keys {
value, err := client.Get(k, false)
if err != nil {
return false, checkError(err, f.logger, k)
}

index := strings.LastIndex(value, "/")
value = value[index+1:]
node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n")
}

// Get internal and external IP (if it exits)
value, err := client.Get("network-interfaces/0/ip", false)
if err != nil {
return false, checkError(err, f.logger, "ip")
}
newNetwork.IP = strings.Trim(value, "\n")
newNetwork.CIDR = newNetwork.IP + "/32"
node.Attributes["network.ip-address"] = newNetwork.IP

value, err = client.Get("network-interfaces/0/access-configs/0/external-ip", false)
if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) {
return false, checkError(err, f.logger, "external IP")
}
value = strings.Trim(value, "\n")
if len(value) > 0 {
node.Attributes["platform.gce.external-ip"] = value
}

var tagList []string
value, err = client.Get("tags", false)
if err != nil {
return false, checkError(err, f.logger, "tags")
}
err = json.Unmarshal([]byte(value), &tagList)
if err == nil {
for _, tag := range tagList {
node.Attributes["platform.gce.tag."+tag] = "true"
}
} else {
f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error())
}

var attrDict map[string]string
value, err = client.Get("attributes/", true)
if err != nil {
return false, checkError(err, f.logger, "attributes/")
}
err = json.Unmarshal([]byte(value), &attrDict)
if err == nil {
for k, v := range attrDict {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n")
}
} else {
f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error())
}

// populate Node Network Resources
if node.Resources == nil {
node.Resources = &structs.Resources{}
}
node.Resources.Networks = append(node.Resources.Networks, newNetwork)

// populate Links
node.Links["gce"] = node.Attributes["platform.gce.id"]

return true, nil
}

func (f *EnvGCEFingerprint) isGCE() bool {
// TODO: better way to detect GCE?

client := NewGCEMetadataClient(f.logger)
// Query the metadata url for the machine type, to verify we're on GCE
machineType, err := client.Get("machine-type", false)
if err != nil {
if re, ok := err.(ReqError); !ok || re.StatusCode != 404 {
// If it wasn't a 404 error, print an error message.
f.logger.Printf("[ERR] fingerprint.env_gce: Error querying GCE Metadata URL, skipping")
}
return false
}

match, err := regexp.MatchString("projects/.+/machineTypes/.+", machineType)
if !match {
return false
}

return true
}
Loading