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