github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/fingerprint/env_azure.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 "strings" 11 "time" 12 13 cleanhttp "github.com/hashicorp/go-cleanhttp" 14 log "github.com/hashicorp/go-hclog" 15 16 "github.com/hashicorp/nomad/helper/useragent" 17 "github.com/hashicorp/nomad/nomad/structs" 18 ) 19 20 const ( 21 // AzureMetadataURL is where the Azure metadata server normally resides. We hardcode the 22 // "instance" path as well since it's the only one we access here. 23 AzureMetadataURL = "http://169.254.169.254/metadata/instance/" 24 25 // AzureMetadataAPIVersion is the version used when contacting the Azure metadata 26 // services. 27 AzureMetadataAPIVersion = "2019-06-04" 28 29 // AzureMetadataTimeout is the timeout used when contacting the Azure metadata 30 // services. 31 AzureMetadataTimeout = 2 * time.Second 32 ) 33 34 type AzureMetadataTag struct { 35 Name string 36 Value string 37 } 38 39 type AzureMetadataPair struct { 40 path string 41 unique bool 42 } 43 44 // EnvAzureFingerprint is used to fingerprint Azure metadata 45 type EnvAzureFingerprint struct { 46 StaticFingerprinter 47 client *http.Client 48 logger log.Logger 49 metadataURL string 50 } 51 52 // NewEnvAzureFingerprint is used to create a fingerprint from Azure metadata 53 func NewEnvAzureFingerprint(logger log.Logger) Fingerprint { 54 // Read the internal metadata URL from the environment, allowing test files to 55 // provide their own 56 metadataURL := os.Getenv("AZURE_ENV_URL") 57 if metadataURL == "" { 58 metadataURL = AzureMetadataURL 59 } 60 61 // assume 2 seconds is enough time for inside Azure network 62 client := &http.Client{ 63 Timeout: AzureMetadataTimeout, 64 Transport: cleanhttp.DefaultTransport(), 65 } 66 67 return &EnvAzureFingerprint{ 68 client: client, 69 logger: logger.Named("env_azure"), 70 metadataURL: metadataURL, 71 } 72 } 73 74 func (f *EnvAzureFingerprint) Get(attribute string, format string) (string, error) { 75 reqURL := f.metadataURL + attribute + fmt.Sprintf("?api-version=%s&format=%s", AzureMetadataAPIVersion, format) 76 parsedURL, err := url.Parse(reqURL) 77 if err != nil { 78 return "", err 79 } 80 81 req := &http.Request{ 82 Method: "GET", 83 URL: parsedURL, 84 Header: http.Header{ 85 "Metadata": []string{"true"}, 86 "User-Agent": []string{useragent.String()}, 87 }, 88 } 89 90 res, err := f.client.Do(req) 91 if err != nil { 92 f.logger.Debug("could not read value for attribute", "attribute", attribute, "error", err) 93 return "", err 94 } else if res.StatusCode != http.StatusOK { 95 f.logger.Debug("could not read value for attribute", "attribute", attribute, "resp_code", res.StatusCode) 96 return "", err 97 } 98 99 resp, err := ioutil.ReadAll(res.Body) 100 res.Body.Close() 101 if err != nil { 102 f.logger.Error("error reading response body for Azure attribute", "attribute", attribute, "error", err) 103 return "", err 104 } 105 106 if res.StatusCode >= 400 { 107 return "", ReqError{res.StatusCode} 108 } 109 110 return string(resp), nil 111 } 112 113 func checkAzureError(err error, logger log.Logger, desc string) error { 114 // If it's a URL error, assume we're not actually in an Azure environment. 115 // To the outer layers, this isn't an error so return nil. 116 if _, ok := err.(*url.Error); ok { 117 logger.Debug("error querying Azure attribute; skipping", "attribute", desc) 118 return nil 119 } 120 // Otherwise pass the error through. 121 return err 122 } 123 124 func (f *EnvAzureFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error { 125 cfg := request.Config 126 127 // Check if we should tighten the timeout 128 if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) { 129 f.client.Timeout = 1 * time.Millisecond 130 } 131 132 if !f.isAzure() { 133 return nil 134 } 135 136 // Keys and whether they should be namespaced as unique. Any key whose value 137 // uniquely identifies a node, such as ip, should be marked as unique. When 138 // marked as unique, the key isn't included in the computed node class. 139 keys := map[string]AzureMetadataPair{ 140 "id": {unique: true, path: "compute/vmId"}, 141 "name": {unique: true, path: "compute/name"}, // name might not be the same as hostname 142 "location": {unique: false, path: "compute/location"}, 143 "resource-group": {unique: false, path: "compute/resourceGroupName"}, 144 "scale-set": {unique: false, path: "compute/vmScaleSetName"}, 145 "vm-size": {unique: false, path: "compute/vmSize"}, 146 "zone": {unique: false, path: "compute/zone"}, 147 "local-ipv4": {unique: true, path: "network/interface/0/ipv4/ipAddress/0/privateIpAddress"}, 148 "public-ipv4": {unique: true, path: "network/interface/0/ipv4/ipAddress/0/publicIpAddress"}, 149 "local-ipv6": {unique: true, path: "network/interface/0/ipv6/ipAddress/0/privateIpAddress"}, 150 "public-ipv6": {unique: true, path: "network/interface/0/ipv6/ipAddress/0/publicIpAddress"}, 151 "mac": {unique: true, path: "network/interface/0/macAddress"}, 152 } 153 154 for k, attr := range keys { 155 resp, err := f.Get(attr.path, "text") 156 v := strings.TrimSpace(resp) 157 if err != nil { 158 return checkAzureError(err, f.logger, k) 159 } else if v == "" { 160 f.logger.Debug("read an empty value", "attribute", k) 161 continue 162 } 163 164 // assume we want blank entries 165 key := "platform.azure." + strings.ReplaceAll(k, "/", ".") 166 if attr.unique { 167 key = structs.UniqueNamespace(key) 168 } 169 response.AddAttribute(key, v) 170 } 171 172 // copy over network specific information 173 if val, ok := response.Attributes["unique.platform.azure.local-ipv4"]; ok && val != "" { 174 response.AddAttribute("unique.network.ip-address", val) 175 } 176 177 var tagList []AzureMetadataTag 178 value, err := f.Get("compute/tagsList", "json") 179 if err != nil { 180 return checkAzureError(err, f.logger, "tags") 181 } 182 if err := json.Unmarshal([]byte(value), &tagList); err != nil { 183 f.logger.Warn("error decoding instance tags", "error", err) 184 } 185 for _, tag := range tagList { 186 attr := "platform.azure.tag." 187 var key string 188 189 // If the tag is namespaced as unique, we strip it from the tag and 190 // prepend to the whole attribute. 191 if structs.IsUniqueNamespace(tag.Name) { 192 tag.Name = strings.TrimPrefix(tag.Name, structs.NodeUniqueNamespace) 193 key = fmt.Sprintf("%s%s%s", structs.NodeUniqueNamespace, attr, tag.Name) 194 } else { 195 key = fmt.Sprintf("%s%s", attr, tag.Name) 196 } 197 198 response.AddAttribute(key, tag.Value) 199 } 200 201 // populate Links 202 if id, ok := response.Attributes["unique.platform.azure.id"]; ok { 203 response.AddLink("azure", id) 204 } 205 206 response.Detected = true 207 return nil 208 } 209 210 func (f *EnvAzureFingerprint) isAzure() bool { 211 v, err := f.Get("compute/azEnvironment", "text") 212 v = strings.TrimSpace(v) 213 return err == nil && v != "" 214 }