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
9 changes: 7 additions & 2 deletions client/fingerprint/env_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ var ec2InstanceSpeedMap = map[string]int{
"d2.8xlarge": 10000,
}

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

// NewEnvAWSFingerprint is used to create a CPU fingerprint
// NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata
func NewEnvAWSFingerprint(logger *log.Logger) Fingerprint {
f := &EnvAWSFingerprint{logger: logger}
return f
Expand Down 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
2 changes: 2 additions & 0 deletions client/fingerprint/env_aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

func TestEnvAWSFingerprint_nonAws(t *testing.T) {
os.Setenv("AWS_ENV_URL", "http://127.0.0.1/latest/meta-data/")
f := NewEnvAWSFingerprint(testLogger())
node := &structs.Node{
Attributes: make(map[string]string),
Expand Down Expand Up @@ -199,6 +200,7 @@ func TestNetworkFingerprint_AWS(t *testing.T) {
}

func TestNetworkFingerprint_notAWS(t *testing.T) {
os.Setenv("AWS_ENV_URL", "http://127.0.0.1/latest/meta-data/")
f := NewEnvAWSFingerprint(testLogger())
node := &structs.Node{
Attributes: make(map[string]string),
Expand Down
229 changes: 229 additions & 0 deletions client/fingerprint/env_gce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package fingerprint

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

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

// This is where the GCE metadata server normally resides. We hardcode the
// "instance" path as well since it's the only one we access here.
const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/"

type GCEMetadataNetworkInterface struct {
AccessConfigs []struct {
ExternalIp string
Type string
}
ForwardedIps []string
Ip string
Network string
}

type ReqError struct {
StatusCode int
}

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

func lastToken(s string) string {
index := strings.LastIndex(s, "/")
return s[index+1:]
}

// EnvGCEFingerprint is used to fingerprint GCE metadata
type EnvGCEFingerprint struct {
client *http.Client
logger *log.Logger
metadataURL string
}

// NewEnvGCEFingerprint is used to create a fingerprint from GCE metadata
func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint {
// Read the internal metadata URL from the environment, allowing test files to
// provide their own
metadataURL := os.Getenv("GCE_ENV_URL")
if metadataURL == "" {
metadataURL = DEFAULT_GCE_URL
}

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

return &EnvGCEFingerprint{
client: client,
logger: logger,
metadataURL: metadataURL,
}
}

func (f *EnvGCEFingerprint) Get(attribute string, recursive bool) (string, error) {
reqUrl := f.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 := f.client.Do(req)
if err != nil {
return "", err
}

resp, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
f.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
}

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
}

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

keys := []string{
"hostname",
"id",
"cpu-platform",
"scheduling/automatic-restart",
"scheduling/on-host-maintenance",
}
for _, k := range keys {
value, err := f.Get(k, false)
if err != nil {
return false, checkError(err, f.logger, k)
}

// assume we want blank entries
key := strings.Replace(k, "/", ".", -1)
node.Attributes["platform.gce."+key] = 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 := f.Get(k, false)
if err != nil {
return false, checkError(err, f.logger, k)
}

node.Attributes["platform.gce."+k] = strings.Trim(lastToken(value), "\n")
}

// Get internal and external IPs (if they exist)
value, err := f.Get("network-interfaces/", true)
var interfaces []GCEMetadataNetworkInterface
if err := json.Unmarshal([]byte(value), &interfaces); err != nil {
f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error())
} else {
for _, intf := range interfaces {
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

Copy link
Contributor

Choose a reason for hiding this comment

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

@dimfeld: Yeah I deleted because I realized exactly what you wrote. I spent some time reading the GCE docs and create an instance to play with and I agree with what you did in this PR. We will just use the network_unix.go. Just small comments and then we will merge this!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool sounds great! Just took care of the one above.

prefix := "platform.gce.network." + lastToken(intf.Network)
node.Attributes[prefix] = "true"
node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n")
for index, accessConfig := range intf.AccessConfigs {
node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp
}
}
}

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

var attrDict map[string]string
value, err = f.Get("attributes/", true)
if err != nil {
return false, checkError(err, f.logger, "attributes/")
}
if err := json.Unmarshal([]byte(value), &attrDict); err != nil {
f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error())
}
for k, v := range attrDict {
node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n")
}

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

return true, nil
}

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

// Query the metadata url for the machine type, to verify we're on GCE
machineType, err := f.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