Skip to content

Commit 8c2f407

Browse files
lcherukafredrikhgrelland
authored andcommitted
client: added azure fingerprinting support (hashicorp#8979)
1 parent 7708ffe commit 8c2f407

File tree

3 files changed

+435
-2
lines changed

3 files changed

+435
-2
lines changed

client/fingerprint/env_azure.go

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
"hostname": {unique: true, path: "compute/name"},
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+
"local-ipv4": {unique: true, path: "network/interface/0/ipv4/ipAddress/0/privateIpAddress"},
147+
"public-ipv4": {unique: true, path: "network/interface/0/ipv4/ipAddress/0/publicIpAddress"},
148+
"local-ipv6": {unique: true, path: "network/interface/0/ipv6/ipAddress/0/privateIpAddress"},
149+
"public-ipv6": {unique: true, path: "network/interface/0/ipv6/ipAddress/0/publicIpAddress"},
150+
"mac": {unique: true, path: "network/interface/0/macAddress"},
151+
}
152+
153+
for k, attr := range keys {
154+
resp, err := f.Get(attr.path, "text")
155+
v := strings.TrimSpace(resp)
156+
if err != nil {
157+
return checkAzureError(err, f.logger, k)
158+
} else if v == "" {
159+
f.logger.Debug("read an empty value", "attribute", k)
160+
continue
161+
}
162+
163+
// assume we want blank entries
164+
key := "platform.azure." + strings.Replace(k, "/", ".", -1)
165+
if attr.unique {
166+
key = structs.UniqueNamespace(key)
167+
}
168+
response.AddAttribute(key, v)
169+
}
170+
171+
// copy over network specific information
172+
if val, ok := response.Attributes["unique.platform.azure.local-ipv4"]; ok && val != "" {
173+
response.AddAttribute("unique.network.ip-address", val)
174+
}
175+
176+
var tagList []AzureMetadataTag
177+
value, err := f.Get("compute/tagsList", "json")
178+
if err != nil {
179+
return checkAzureError(err, f.logger, "tags")
180+
}
181+
if err := json.Unmarshal([]byte(value), &tagList); err != nil {
182+
f.logger.Warn("error decoding instance tags", "error", err)
183+
}
184+
for _, tag := range tagList {
185+
attr := "platform.azure.tag."
186+
var key string
187+
188+
// If the tag is namespaced as unique, we strip it from the tag and
189+
// prepend to the whole attribute.
190+
if structs.IsUniqueNamespace(tag.Name) {
191+
tag.Name = strings.TrimPrefix(tag.Name, structs.NodeUniqueNamespace)
192+
key = fmt.Sprintf("%s%s%s", structs.NodeUniqueNamespace, attr, tag.Name)
193+
} else {
194+
key = fmt.Sprintf("%s%s", attr, tag.Name)
195+
}
196+
197+
response.AddAttribute(key, tag.Value)
198+
}
199+
200+
// populate Links
201+
if id, ok := response.Attributes["unique.platform.azure.id"]; ok {
202+
response.AddLink("azure", id)
203+
}
204+
205+
response.Detected = true
206+
return nil
207+
}
208+
209+
func (f *EnvAzureFingerprint) isAzure() bool {
210+
v, err := f.Get("compute/azEnvironment", "text")
211+
v = strings.TrimSpace(v)
212+
return err == nil && v != ""
213+
}

0 commit comments

Comments
 (0)