github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/fingerprint/env_aws_test.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"testing"
     8  
     9  	"github.com/hashicorp/nomad/ci"
    10  	"github.com/hashicorp/nomad/client/config"
    11  	"github.com/hashicorp/nomad/helper/testlog"
    12  	"github.com/hashicorp/nomad/nomad/structs"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func TestEnvAWSFingerprint_nonAws(t *testing.T) {
    17  	ci.Parallel(t)
    18  
    19  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
    20  	f.(*EnvAWSFingerprint).endpoint = "http://127.0.0.1/latest"
    21  
    22  	node := &structs.Node{
    23  		Attributes: make(map[string]string),
    24  	}
    25  
    26  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
    27  	var response FingerprintResponse
    28  	err := f.Fingerprint(request, &response)
    29  	require.NoError(t, err)
    30  	require.Empty(t, response.Attributes)
    31  }
    32  
    33  func TestEnvAWSFingerprint_aws(t *testing.T) {
    34  	ci.Parallel(t)
    35  
    36  	endpoint, cleanup := startFakeEC2Metadata(t, awsStubs)
    37  	defer cleanup()
    38  
    39  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
    40  	f.(*EnvAWSFingerprint).endpoint = endpoint
    41  
    42  	node := &structs.Node{
    43  		Attributes: make(map[string]string),
    44  	}
    45  
    46  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
    47  	var response FingerprintResponse
    48  	err := f.Fingerprint(request, &response)
    49  	require.NoError(t, err)
    50  
    51  	keys := []string{
    52  		"platform.aws.ami-id",
    53  		"unique.platform.aws.hostname",
    54  		"unique.platform.aws.instance-id",
    55  		"platform.aws.instance-life-cycle",
    56  		"platform.aws.instance-type",
    57  		"unique.platform.aws.local-hostname",
    58  		"unique.platform.aws.local-ipv4",
    59  		"unique.platform.aws.public-hostname",
    60  		"unique.platform.aws.public-ipv4",
    61  		"platform.aws.placement.availability-zone",
    62  		"unique.network.ip-address",
    63  	}
    64  
    65  	for _, k := range keys {
    66  		assertNodeAttributeContains(t, response.Attributes, k)
    67  	}
    68  
    69  	require.NotEmpty(t, response.Links)
    70  
    71  	// confirm we have at least instance-id and ami-id
    72  	for _, k := range []string{"aws.ec2"} {
    73  		assertNodeLinksContains(t, response.Links, k)
    74  	}
    75  }
    76  
    77  func TestNetworkFingerprint_AWS(t *testing.T) {
    78  	ci.Parallel(t)
    79  
    80  	endpoint, cleanup := startFakeEC2Metadata(t, awsStubs)
    81  	defer cleanup()
    82  
    83  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
    84  	f.(*EnvAWSFingerprint).endpoint = endpoint
    85  
    86  	node := &structs.Node{
    87  		Attributes: make(map[string]string),
    88  	}
    89  
    90  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
    91  	var response FingerprintResponse
    92  	err := f.Fingerprint(request, &response)
    93  	require.NoError(t, err)
    94  
    95  	assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address")
    96  
    97  	require.NotNil(t, response.NodeResources)
    98  	require.Len(t, response.NodeResources.Networks, 1)
    99  
   100  	// Test at least the first Network Resource
   101  	net := response.NodeResources.Networks[0]
   102  	require.NotEmpty(t, net.IP, "Expected Network Resource to have an IP")
   103  	require.NotEmpty(t, net.CIDR, "Expected Network Resource to have a CIDR")
   104  	require.NotEmpty(t, net.Device, "Expected Network Resource to have a Device Name")
   105  }
   106  
   107  func TestNetworkFingerprint_AWS_network(t *testing.T) {
   108  	ci.Parallel(t)
   109  
   110  	endpoint, cleanup := startFakeEC2Metadata(t, awsStubs)
   111  	defer cleanup()
   112  
   113  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
   114  	f.(*EnvAWSFingerprint).endpoint = endpoint
   115  
   116  	{
   117  		node := &structs.Node{
   118  			Attributes: make(map[string]string),
   119  		}
   120  
   121  		request := &FingerprintRequest{Config: &config.Config{}, Node: node}
   122  		var response FingerprintResponse
   123  		err := f.Fingerprint(request, &response)
   124  		require.NoError(t, err)
   125  
   126  		require.True(t, response.Detected, "expected response to be applicable")
   127  
   128  		assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address")
   129  
   130  		require.NotNil(t, response.NodeResources)
   131  		require.Len(t, response.NodeResources.Networks, 1)
   132  
   133  		// Test at least the first Network Resource
   134  		net := response.NodeResources.Networks[0]
   135  		require.NotEmpty(t, net.IP, "Expected Network Resource to have an IP")
   136  		require.NotEmpty(t, net.CIDR, "Expected Network Resource to have a CIDR")
   137  		require.NotEmpty(t, net.Device, "Expected Network Resource to have a Device Name")
   138  		require.Equal(t, 1000, net.MBits)
   139  	}
   140  
   141  	// Try again this time setting a network speed in the config
   142  	{
   143  		node := &structs.Node{
   144  			Attributes: make(map[string]string),
   145  		}
   146  
   147  		cfg := &config.Config{
   148  			NetworkSpeed: 10,
   149  		}
   150  
   151  		request := &FingerprintRequest{Config: cfg, Node: node}
   152  		var response FingerprintResponse
   153  		err := f.Fingerprint(request, &response)
   154  		require.NoError(t, err)
   155  
   156  		assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address")
   157  
   158  		require.NotNil(t, response.NodeResources)
   159  		require.Len(t, response.NodeResources.Networks, 1)
   160  
   161  		// Test at least the first Network Resource
   162  		net := response.NodeResources.Networks[0]
   163  		require.NotEmpty(t, net.IP, "Expected Network Resource to have an IP")
   164  		require.NotEmpty(t, net.CIDR, "Expected Network Resource to have a CIDR")
   165  		require.NotEmpty(t, net.Device, "Expected Network Resource to have a Device Name")
   166  		require.Equal(t, 10, net.MBits)
   167  	}
   168  }
   169  
   170  func TestNetworkFingerprint_AWS_NoNetwork(t *testing.T) {
   171  	ci.Parallel(t)
   172  
   173  	endpoint, cleanup := startFakeEC2Metadata(t, noNetworkAWSStubs)
   174  	defer cleanup()
   175  
   176  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
   177  	f.(*EnvAWSFingerprint).endpoint = endpoint
   178  
   179  	node := &structs.Node{
   180  		Attributes: make(map[string]string),
   181  	}
   182  
   183  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
   184  	var response FingerprintResponse
   185  	err := f.Fingerprint(request, &response)
   186  	require.NoError(t, err)
   187  
   188  	require.True(t, response.Detected, "expected response to be applicable")
   189  
   190  	require.Equal(t, "ami-1234", response.Attributes["platform.aws.ami-id"])
   191  
   192  	require.Nil(t, response.NodeResources.Networks)
   193  }
   194  
   195  func TestNetworkFingerprint_AWS_IncompleteImitation(t *testing.T) {
   196  	ci.Parallel(t)
   197  
   198  	endpoint, cleanup := startFakeEC2Metadata(t, incompleteAWSImitationStubs)
   199  	defer cleanup()
   200  
   201  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
   202  	f.(*EnvAWSFingerprint).endpoint = endpoint
   203  
   204  	node := &structs.Node{
   205  		Attributes: make(map[string]string),
   206  	}
   207  
   208  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
   209  	var response FingerprintResponse
   210  	err := f.Fingerprint(request, &response)
   211  	require.NoError(t, err)
   212  
   213  	require.False(t, response.Detected, "expected response not to be applicable")
   214  
   215  	require.NotContains(t, response.Attributes, "platform.aws.ami-id")
   216  	require.Nil(t, response.NodeResources)
   217  }
   218  
   219  func TestCPUFingerprint_AWS_InstanceFound(t *testing.T) {
   220  	ci.Parallel(t)
   221  
   222  	endpoint, cleanup := startFakeEC2Metadata(t, awsStubs)
   223  	defer cleanup()
   224  
   225  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
   226  	f.(*EnvAWSFingerprint).endpoint = endpoint
   227  
   228  	node := &structs.Node{Attributes: make(map[string]string)}
   229  
   230  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
   231  	var response FingerprintResponse
   232  	err := f.Fingerprint(request, &response)
   233  	require.NoError(t, err)
   234  	require.True(t, response.Detected)
   235  	require.Equal(t, "2200", response.Attributes["cpu.frequency"])
   236  	require.Equal(t, "8", response.Attributes["cpu.numcores"])
   237  	require.Equal(t, "17600", response.Attributes["cpu.totalcompute"])
   238  	require.Equal(t, 17600, response.Resources.CPU)
   239  	require.Equal(t, int64(17600), response.NodeResources.Cpu.CpuShares)
   240  }
   241  
   242  func TestCPUFingerprint_AWS_OverrideCompute(t *testing.T) {
   243  	ci.Parallel(t)
   244  
   245  	endpoint, cleanup := startFakeEC2Metadata(t, awsStubs)
   246  	defer cleanup()
   247  
   248  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
   249  	f.(*EnvAWSFingerprint).endpoint = endpoint
   250  
   251  	node := &structs.Node{Attributes: make(map[string]string)}
   252  
   253  	request := &FingerprintRequest{Config: &config.Config{
   254  		CpuCompute: 99999,
   255  	}, Node: node}
   256  	var response FingerprintResponse
   257  	err := f.Fingerprint(request, &response)
   258  	require.NoError(t, err)
   259  	require.True(t, response.Detected)
   260  	require.Equal(t, "2200", response.Attributes["cpu.frequency"])
   261  	require.Equal(t, "8", response.Attributes["cpu.numcores"])
   262  	require.Equal(t, "99999", response.Attributes["cpu.totalcompute"])
   263  	require.Nil(t, response.Resources)          // defaults in cpu fingerprinter
   264  	require.Zero(t, response.NodeResources.Cpu) // defaults in cpu fingerprinter
   265  }
   266  
   267  func TestCPUFingerprint_AWS_InstanceNotFound(t *testing.T) {
   268  	ci.Parallel(t)
   269  
   270  	endpoint, cleanup := startFakeEC2Metadata(t, unknownInstanceType)
   271  	defer cleanup()
   272  
   273  	f := NewEnvAWSFingerprint(testlog.HCLogger(t))
   274  	f.(*EnvAWSFingerprint).endpoint = endpoint
   275  
   276  	node := &structs.Node{Attributes: make(map[string]string)}
   277  
   278  	request := &FingerprintRequest{Config: &config.Config{}, Node: node}
   279  	var response FingerprintResponse
   280  	err := f.Fingerprint(request, &response)
   281  	require.NoError(t, err)
   282  	require.True(t, response.Detected)
   283  	require.NotContains(t, response.Attributes, "cpu.modelname")
   284  	require.NotContains(t, response.Attributes, "cpu.frequency")
   285  	require.NotContains(t, response.Attributes, "cpu.numcores")
   286  	require.NotContains(t, response.Attributes, "cpu.totalcompute")
   287  	require.Nil(t, response.Resources)
   288  	require.Nil(t, response.NodeResources)
   289  }
   290  
   291  /// Utility functions for tests
   292  
   293  func startFakeEC2Metadata(t *testing.T, endpoints []endpoint) (endpoint string, cleanup func()) {
   294  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   295  		for _, e := range endpoints {
   296  			if r.RequestURI == e.Uri {
   297  				w.Header().Set("Content-Type", e.ContentType)
   298  				fmt.Fprintln(w, e.Body)
   299  			}
   300  		}
   301  	}))
   302  
   303  	return ts.URL + "/latest", ts.Close
   304  }
   305  
   306  type routes struct {
   307  	Endpoints []*endpoint `json:"endpoints"`
   308  }
   309  
   310  type endpoint struct {
   311  	Uri         string `json:"uri"`
   312  	ContentType string `json:"content-type"`
   313  	Body        string `json:"body"`
   314  }
   315  
   316  // awsStubs mimics normal EC2 instance metadata
   317  var awsStubs = []endpoint{
   318  	{
   319  		Uri:         "/latest/meta-data/ami-id",
   320  		ContentType: "text/plain",
   321  		Body:        "ami-1234",
   322  	},
   323  	{
   324  		Uri:         "/latest/meta-data/hostname",
   325  		ContentType: "text/plain",
   326  		Body:        "ip-10-0-0-207.us-west-2.compute.internal",
   327  	},
   328  	{
   329  		Uri:         "/latest/meta-data/placement/availability-zone",
   330  		ContentType: "text/plain",
   331  		Body:        "us-west-2a",
   332  	},
   333  	{
   334  		Uri:         "/latest/meta-data/instance-id",
   335  		ContentType: "text/plain",
   336  		Body:        "i-b3ba3875",
   337  	},
   338  	{
   339  		Uri:         "/latest/meta-data/instance-life-cycle",
   340  		ContentType: "text/plain",
   341  		Body:        "on-demand",
   342  	},
   343  	{
   344  		Uri:         "/latest/meta-data/instance-type",
   345  		ContentType: "text/plain",
   346  		Body:        "t3a.2xlarge",
   347  	},
   348  	{
   349  		Uri:         "/latest/meta-data/local-hostname",
   350  		ContentType: "text/plain",
   351  		Body:        "ip-10-0-0-207.us-west-2.compute.internal",
   352  	},
   353  	{
   354  		Uri:         "/latest/meta-data/local-ipv4",
   355  		ContentType: "text/plain",
   356  		Body:        "10.0.0.207",
   357  	},
   358  	{
   359  		Uri:         "/latest/meta-data/public-hostname",
   360  		ContentType: "text/plain",
   361  		Body:        "ec2-54-191-117-175.us-west-2.compute.amazonaws.com",
   362  	},
   363  	{
   364  		Uri:         "/latest/meta-data/public-ipv4",
   365  		ContentType: "text/plain",
   366  		Body:        "54.191.117.175",
   367  	},
   368  	{
   369  		Uri:         "/latest/meta-data/mac",
   370  		ContentType: "text/plain",
   371  		Body:        "0a:20:d2:42:b3:55",
   372  	},
   373  }
   374  
   375  var unknownInstanceType = []endpoint{
   376  	{
   377  		Uri:         "/latest/meta-data/ami-id",
   378  		ContentType: "text/plain",
   379  		Body:        "ami-1234",
   380  	},
   381  	{
   382  		Uri:         "/latest/meta-data/hostname",
   383  		ContentType: "text/plain",
   384  		Body:        "ip-10-0-0-207.us-west-2.compute.internal",
   385  	},
   386  	{
   387  		Uri:         "/latest/meta-data/placement/availability-zone",
   388  		ContentType: "text/plain",
   389  		Body:        "us-west-2a",
   390  	},
   391  	{
   392  		Uri:         "/latest/meta-data/instance-id",
   393  		ContentType: "text/plain",
   394  		Body:        "i-b3ba3875",
   395  	},
   396  	{
   397  		Uri:         "/latest/meta-data/instance-life-cycle",
   398  		ContentType: "text/plain",
   399  		Body:        "on-demand",
   400  	},
   401  	{
   402  		Uri:         "/latest/meta-data/instance-type",
   403  		ContentType: "text/plain",
   404  		Body:        "xyz123.uber",
   405  	},
   406  }
   407  
   408  // noNetworkAWSStubs mimics an EC2 instance but without local ip address
   409  // may happen in environments with odd EC2 Metadata emulation
   410  var noNetworkAWSStubs = []endpoint{
   411  	{
   412  		Uri:         "/latest/meta-data/ami-id",
   413  		ContentType: "text/plain",
   414  		Body:        "ami-1234",
   415  	},
   416  	{
   417  		Uri:         "/latest/meta-data/hostname",
   418  		ContentType: "text/plain",
   419  		Body:        "ip-10-0-0-207.us-west-2.compute.internal",
   420  	},
   421  	{
   422  		Uri:         "/latest/meta-data/placement/availability-zone",
   423  		ContentType: "text/plain",
   424  		Body:        "us-west-2a",
   425  	},
   426  	{
   427  		Uri:         "/latest/meta-data/instance-id",
   428  		ContentType: "text/plain",
   429  		Body:        "i-b3ba3875",
   430  	},
   431  	{
   432  		Uri:         "/latest/meta-data/instance-life-cycle",
   433  		ContentType: "text/plain",
   434  		Body:        "on-demand",
   435  	},
   436  	{
   437  		Uri:         "/latest/meta-data/instance-type",
   438  		ContentType: "text/plain",
   439  		Body:        "m3.2xlarge",
   440  	},
   441  	{
   442  		Uri:         "/latest/meta-data/local-hostname",
   443  		ContentType: "text/plain",
   444  		Body:        "ip-10-0-0-207.us-west-2.compute.internal",
   445  	},
   446  	{
   447  		Uri:         "/latest/meta-data/local-ipv4",
   448  		ContentType: "text/plain",
   449  		Body:        "",
   450  	},
   451  	{
   452  		Uri:         "/latest/meta-data/public-hostname",
   453  		ContentType: "text/plain",
   454  		Body:        "ec2-54-191-117-175.us-west-2.compute.amazonaws.com",
   455  	},
   456  	{
   457  		Uri:         "/latest/meta-data/public-ipv4",
   458  		ContentType: "text/plain",
   459  		Body:        "54.191.117.175",
   460  	},
   461  }
   462  
   463  // incompleteAWSImitationsStub mimics environments where some AWS endpoints
   464  // return empty, namely Hetzner
   465  var incompleteAWSImitationStubs = []endpoint{
   466  	{
   467  		Uri:         "/latest/meta-data/hostname",
   468  		ContentType: "text/plain",
   469  		Body:        "ip-10-0-0-207.us-west-2.compute.internal",
   470  	},
   471  	{
   472  		Uri:         "/latest/meta-data/instance-id",
   473  		ContentType: "text/plain",
   474  		Body:        "i-b3ba3875",
   475  	},
   476  	{
   477  		Uri:         "/latest/meta-data/local-ipv4",
   478  		ContentType: "text/plain",
   479  		Body:        "",
   480  	},
   481  	{
   482  		Uri:         "/latest/meta-data/public-ipv4",
   483  		ContentType: "text/plain",
   484  		Body:        "54.191.117.175",
   485  	},
   486  }