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  }