Skip to content

Commit e4e92ca

Browse files
Make nma api calls and imds call to update delegated NC goal state
update values add client test scenarios . . . . .
1 parent 1c28add commit e4e92ca

File tree

7 files changed

+650
-16
lines changed

7 files changed

+650
-16
lines changed

cns/fakes/imdsclientfake.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,15 @@ func (m *MockIMDSClient) GetVMUniqueID(ctx context.Context) (string, error) {
5757

5858
return "55b8499d-9b42-4f85-843f-24ff69f4a643", nil
5959
}
60+
61+
func (m *MockIMDSClient) GetNCVersionsFromIMDS(ctx context.Context) (map[string]string, error) {
62+
if ctx.Value(SimulateError) != nil {
63+
return nil, imds.ErrUnexpectedStatusCode
64+
}
65+
66+
// Return some mock NC versions for testing
67+
return map[string]string{
68+
"nc1": "1",
69+
"nc2": "2",
70+
}, nil
71+
}

cns/imds/client.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ func RetryAttempts(attempts uint) ClientOption {
4646
const (
4747
vmUniqueIDProperty = "vmId"
4848
imdsComputePath = "/metadata/instance/compute"
49+
imdsNetworkPath = "/metadata/instance/network"
4950
imdsComputeAPIVersion = "api-version=2021-01-01"
5051
imdsFormatJSON = "format=json"
5152
metadataHeaderKey = "Metadata"
5253
metadataHeaderValue = "true"
5354
defaultRetryAttempts = 3
5455
defaultIMDSEndpoint = "http://169.254.169.254"
56+
ncVersion = "ncVersion"
5557
)
5658

5759
var (
@@ -79,7 +81,7 @@ func NewClient(opts ...ClientOption) *Client {
7981
func (c *Client) GetVMUniqueID(ctx context.Context) (string, error) {
8082
var vmUniqueID string
8183
err := retry.Do(func() error {
82-
computeDoc, err := c.getInstanceComputeMetadata(ctx)
84+
computeDoc, err := c.getInstanceMetadata(ctx, imdsComputePath)
8385
if err != nil {
8486
return errors.Wrap(err, "error getting IMDS compute metadata")
8587
}
@@ -102,10 +104,50 @@ func (c *Client) GetVMUniqueID(ctx context.Context) (string, error) {
102104
return vmUniqueID, nil
103105
}
104106

105-
func (c *Client) getInstanceComputeMetadata(ctx context.Context) (map[string]any, error) {
107+
func (c *Client) GetNCVersionsFromIMDS(ctx context.Context) (map[string]string, error) {
108+
var networkData NetworkMetadata
109+
err := retry.Do(func() error {
110+
networkMetadata, err := c.getInstanceMetadata(ctx, imdsNetworkPath)
111+
if err != nil {
112+
return errors.Wrap(err, "error getting IMDS network metadata")
113+
}
114+
115+
// Try to parse the network metadata as the expected structure
116+
// Convert the map to JSON and back to properly unmarshal into struct
117+
jsonData, err := json.Marshal(networkMetadata)
118+
if err != nil {
119+
return errors.Wrap(err, "error marshaling network metadata")
120+
}
121+
122+
if err := json.Unmarshal(jsonData, &networkData); err != nil {
123+
return errors.Wrap(err, "error unmarshaling network metadata")
124+
}
125+
return nil
126+
}, retry.Context(ctx), retry.Attempts(c.config.retryAttempts), retry.DelayType(retry.BackOffDelay))
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
ncVersions := make(map[string]string)
132+
for _, iface := range networkData.Interface {
133+
// IMDS only returns compartment fields (interfaceCompartmentId, interfaceCompartmentVersion)
134+
// We map these to NC ID and NC version concepts
135+
// Standard fields (ncId, ncVersion) are ignored even if present
136+
ncId := iface.InterfaceCompartmentId
137+
ncVersion := iface.InterfaceCompartmentVersion
138+
139+
if ncId != "" {
140+
ncVersions[ncId] = ncVersion
141+
}
142+
}
143+
144+
return ncVersions, nil
145+
}
146+
147+
func (c *Client) getInstanceMetadata(ctx context.Context, imdsComputePath string) (map[string]any, error) {
106148
imdsComputeURL, err := url.JoinPath(c.config.endpoint, imdsComputePath)
107149
if err != nil {
108-
return nil, errors.Wrap(err, "unable to build path to IMDS compute metadata")
150+
return nil, errors.Wrap(err, "unable to build path to IMDS metadata for path"+imdsComputePath)
109151
}
110152
imdsComputeURL = imdsComputeURL + "?" + imdsComputeAPIVersion + "&" + imdsFormatJSON
111153

@@ -133,3 +175,16 @@ func (c *Client) getInstanceComputeMetadata(ctx context.Context) (map[string]any
133175

134176
return m, nil
135177
}
178+
179+
// NetworkInterface represents a network interface from IMDS
180+
type NetworkInterface struct {
181+
MacAddress string `json:"macAddress"`
182+
// IMDS only returns compartment fields - these are mapped to NC ID and NC version concepts
183+
InterfaceCompartmentId string `json:"interfaceCompartmentId,omitempty"`
184+
InterfaceCompartmentVersion string `json:"interfaceCompartmentVersion,omitempty"`
185+
}
186+
187+
// NetworkMetadata represents the network metadata from IMDS
188+
type NetworkMetadata struct {
189+
Interface []NetworkInterface `json:"interface"`
190+
}

cns/imds/client_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,113 @@ func TestInvalidVMUniqueID(t *testing.T) {
100100
require.Error(t, err, "error querying testserver")
101101
require.Equal(t, "", vmUniqueID)
102102
}
103+
104+
func TestGetNCVersionsFromIMDS(t *testing.T) {
105+
networkMetadata := []byte(`{
106+
"interface": [
107+
{
108+
"macAddress": "00:0D:3A:12:34:56",
109+
"interfaceCompartmentVersion": "1",
110+
"interfaceCompartmentId": "nc-12345-67890"
111+
},
112+
{
113+
"macAddress": "00:0D:3A:CD:EF:12",
114+
"interfaceCompartmentVersion": "",
115+
"interfaceCompartmentId": "nc-abcdef-123456"
116+
}
117+
]
118+
}`)
119+
120+
mockIMDSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121+
// request header "Metadata: true" must be present
122+
metadataHeader := r.Header.Get("Metadata")
123+
assert.Equal(t, "true", metadataHeader)
124+
125+
// verify path is network metadata
126+
assert.Contains(t, r.URL.Path, "/metadata/instance/network")
127+
128+
// query params should include apiversion and json format
129+
apiVersion := r.URL.Query().Get("api-version")
130+
assert.Equal(t, "2021-01-01", apiVersion)
131+
format := r.URL.Query().Get("format")
132+
assert.Equal(t, "json", format)
133+
134+
w.WriteHeader(http.StatusOK)
135+
_, writeErr := w.Write(networkMetadata)
136+
require.NoError(t, writeErr, "error writing response")
137+
}))
138+
defer mockIMDSServer.Close()
139+
140+
imdsClient := imds.NewClient(imds.Endpoint(mockIMDSServer.URL))
141+
ncVersions, err := imdsClient.GetNCVersionsFromIMDS(context.Background())
142+
require.NoError(t, err, "error querying testserver")
143+
144+
expectedNCVersions := map[string]string{
145+
"nc-12345-67890": "1",
146+
"nc-abcdef-123456": "", // empty version
147+
}
148+
require.Equal(t, expectedNCVersions, ncVersions)
149+
}
150+
151+
func TestGetNCVersionsFromIMDSInvalidEndpoint(t *testing.T) {
152+
imdsClient := imds.NewClient(imds.Endpoint(string([]byte{0x7f})), imds.RetryAttempts(1))
153+
_, err := imdsClient.GetNCVersionsFromIMDS(context.Background())
154+
require.Error(t, err, "expected invalid path")
155+
}
156+
157+
func TestGetNCVersionsFromIMDSInvalidJSON(t *testing.T) {
158+
mockIMDSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
159+
w.WriteHeader(http.StatusOK)
160+
_, err := w.Write([]byte("not json"))
161+
require.NoError(t, err)
162+
}))
163+
defer mockIMDSServer.Close()
164+
165+
imdsClient := imds.NewClient(imds.Endpoint(mockIMDSServer.URL), imds.RetryAttempts(1))
166+
_, err := imdsClient.GetNCVersionsFromIMDS(context.Background())
167+
require.Error(t, err, "expected json decoding error")
168+
}
169+
170+
func TestGetNCVersionsFromIMDSNoNCIDs(t *testing.T) {
171+
networkMetadataNoNC := []byte(`{
172+
"interface": [
173+
{
174+
"macAddress": "00:0D:3A:12:34:56",
175+
"ipv4": {
176+
"ipAddress": [
177+
{
178+
"privateIpAddress": "10.0.0.4",
179+
"publicIpAddress": ""
180+
}
181+
]
182+
}
183+
},
184+
{
185+
"macAddress": "00:0D:3A:78:90:AB",
186+
"ipv4": {
187+
"ipAddress": [
188+
{
189+
"privateIpAddress": "10.0.1.4",
190+
"publicIpAddress": ""
191+
}
192+
]
193+
}
194+
}
195+
]
196+
}`)
197+
198+
mockIMDSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
199+
metadataHeader := r.Header.Get("Metadata")
200+
assert.Equal(t, "true", metadataHeader)
201+
202+
w.WriteHeader(http.StatusOK)
203+
_, writeErr := w.Write(networkMetadataNoNC)
204+
require.NoError(t, writeErr, "error writing response")
205+
}))
206+
defer mockIMDSServer.Close()
207+
208+
imdsClient := imds.NewClient(imds.Endpoint(mockIMDSServer.URL))
209+
ncVersions, err := imdsClient.GetNCVersionsFromIMDS(context.Background())
210+
require.NoError(t, err, "error querying testserver")
211+
require.Empty(t, ncVersions, "expected empty NC versions map when no NC IDs present")
212+
}

cns/restserver/internalapi.go

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import (
2525
"github.com/pkg/errors"
2626
)
2727

28+
const (
29+
// Known API names we care about
30+
nmAgentSwiftV2API = "SwiftV2DhcpRehydrationFromGoalState"
31+
)
32+
2833
// This file contains the internal functions called by either HTTP APIs (api.go) or
2934
// internal APIs (definde in internalapi.go).
3035
// This will be used internally (say by RequestController in case of AKS)
@@ -167,6 +172,7 @@ func (service *HTTPRestService) SyncHostNCVersion(ctx context.Context, channelMo
167172
service.Lock()
168173
defer service.Unlock()
169174
start := time.Now()
175+
170176
programmedNCCount, err := service.syncHostNCVersion(ctx, channelMode)
171177
// even if we get an error, we want to write the CNI conflist if we have any NC programmed to any version
172178
if programmedNCCount > 0 {
@@ -202,6 +208,7 @@ func (service *HTTPRestService) syncHostNCVersion(ctx context.Context, channelMo
202208
logger.Errorf("Received err when change nc version %s in containerstatus to int, err msg %v", service.state.ContainerStatus[idx].CreateNetworkContainerRequest.Version, err)
203209
continue
204210
}
211+
logger.Printf("NC %s: local NC version %d, DNC NC version %d", service.state.ContainerStatus[idx].ID, localNCVersion, dncNCVersion)
205212
// host NC version is the NC version from NMAgent, if it's smaller than NC version from DNC, then append it to indicate it needs update.
206213
if localNCVersion < dncNCVersion {
207214
outdatedNCs[service.state.ContainerStatus[idx].ID] = struct{}{}
@@ -216,23 +223,43 @@ func (service *HTTPRestService) syncHostNCVersion(ctx context.Context, channelMo
216223
if len(outdatedNCs) == 0 {
217224
return len(programmedNCs), nil
218225
}
226+
logger.Printf("outdatedNCs: %v", outdatedNCs)
219227
ncVersionListResp, err := service.nma.GetNCVersionList(ctx)
220228
if err != nil {
221229
return len(programmedNCs), errors.Wrap(err, "failed to get nc version list from nmagent")
222230
}
223231

232+
// Get IMDS NC versions for delegated NIC scenarios
233+
imdsNCVersions, err := service.GetIMDSNCVersions(ctx)
234+
if err != nil {
235+
logger.Printf("Failed to get NC versions from IMDS: %v", err)
236+
// If any of the NMA API check calls, imds calls fails assume that nma build doesn't have the latest changes and create empty map
237+
imdsNCVersions = make(map[string]string)
238+
}
239+
224240
nmaNCs := map[string]string{}
225241
for _, nc := range ncVersionListResp.Containers {
226242
nmaNCs[strings.ToLower(nc.NetworkContainerID)] = nc.Version
227243
}
228-
hasNC.Set(float64(len(nmaNCs)))
244+
245+
// Consolidate both maps - NMA takes precedence, IMDS as fallback
246+
consolidatedNCs := make(map[string]string)
247+
for ncID, version := range nmaNCs {
248+
consolidatedNCs[ncID] = version
249+
}
250+
for ncID, version := range imdsNCVersions {
251+
if _, exists := consolidatedNCs[ncID]; !exists {
252+
consolidatedNCs[ncID] = version
253+
}
254+
}
255+
hasNC.Set(float64(len(consolidatedNCs)))
229256
for ncID := range outdatedNCs {
230-
nmaNCVersionStr, ok := nmaNCs[ncID]
257+
consolidatedNCVersionStr, ok := consolidatedNCs[ncID]
231258
if !ok {
232-
// NMA doesn't have this NC that we need programmed yet, bail out
259+
// Neither NMA nor IMDS has this NC that we need programmed yet, bail out
233260
continue
234261
}
235-
nmaNCVersion, err := strconv.Atoi(nmaNCVersionStr)
262+
consolidatedNCVersion, err := strconv.Atoi(consolidatedNCVersionStr)
236263
if err != nil {
237264
logger.Errorf("failed to parse container version of %s: %s", ncID, err)
238265
continue
@@ -245,7 +272,7 @@ func (service *HTTPRestService) syncHostNCVersion(ctx context.Context, channelMo
245272
return len(programmedNCs), errors.Wrapf(errNonExistentContainerStatus, "can't find NC with ID %s in service state, stop updating this host NC version", ncID)
246273
}
247274
// if the NC still exists in state and is programmed to some version (doesn't have to be latest), add it to our set of NCs that have been programmed
248-
if nmaNCVersion > -1 {
275+
if consolidatedNCVersion > -1 {
249276
programmedNCs[ncID] = struct{}{}
250277
}
251278

@@ -254,15 +281,15 @@ func (service *HTTPRestService) syncHostNCVersion(ctx context.Context, channelMo
254281
logger.Errorf("failed to parse host nc version string %s: %s", ncInfo.HostVersion, err)
255282
continue
256283
}
257-
if localNCVersion > nmaNCVersion {
258-
logger.Errorf("NC version from NMA is decreasing: have %d, got %d", localNCVersion, nmaNCVersion)
284+
if localNCVersion > consolidatedNCVersion {
285+
logger.Errorf("NC version from consolidated sources is decreasing: have %d, got %d", localNCVersion, consolidatedNCVersion)
259286
continue
260287
}
261288
if channelMode == cns.CRD {
262-
service.MarkIpsAsAvailableUntransacted(ncInfo.ID, nmaNCVersion)
289+
service.MarkIpsAsAvailableUntransacted(ncInfo.ID, consolidatedNCVersion)
263290
}
264-
logger.Printf("Updating NC %s host version from %s to %s", ncID, ncInfo.HostVersion, nmaNCVersionStr)
265-
ncInfo.HostVersion = nmaNCVersionStr
291+
logger.Printf("Updating NC %s host version from %s to %s", ncID, ncInfo.HostVersion, consolidatedNCVersionStr)
292+
ncInfo.HostVersion = consolidatedNCVersionStr
266293
logger.Printf("Updated NC %s host version to %s", ncID, ncInfo.HostVersion)
267294
service.state.ContainerStatus[ncID] = ncInfo
268295
// if we successfully updated the NC, pop it from the needs update set.
@@ -271,7 +298,7 @@ func (service *HTTPRestService) syncHostNCVersion(ctx context.Context, channelMo
271298
// if we didn't empty out the needs update set, NMA has not programmed all the NCs we are expecting, and we
272299
// need to return an error indicating that
273300
if len(outdatedNCs) > 0 {
274-
return len(programmedNCs), errors.Errorf("unabled to update some NCs: %v, missing or bad response from NMA", outdatedNCs)
301+
return len(programmedNCs), errors.Errorf("unabled to update some NCs: %v, missing or bad response from NMA or ipam", outdatedNCs)
275302
}
276303

277304
return len(programmedNCs), nil
@@ -634,3 +661,59 @@ func (service *HTTPRestService) CreateOrUpdateNetworkContainerInternal(req *cns.
634661
func (service *HTTPRestService) SetVFForAccelnetNICs() error {
635662
return service.setVFForAccelnetNICs()
636663
}
664+
665+
// checkNMAgentAPISupport checks if specific APIs are supported by NMAgent using the existing client
666+
func (service *HTTPRestService) checkNMAgentAPISupport(ctx context.Context) (swiftV2Support bool, err error) {
667+
// Use the existing NMAgent client instead of direct HTTP calls
668+
if service.nma == nil {
669+
return false, fmt.Errorf("NMAgent client is not available")
670+
}
671+
672+
apis, err := service.nma.SupportedAPIs(ctx)
673+
if err != nil {
674+
return false, fmt.Errorf("failed to get supported APIs from NMAgent client: %w", err)
675+
}
676+
677+
logger.Printf("[checkNMAgentAPISupport] Found %d APIs from NMAgent client", len(apis))
678+
for i, api := range apis {
679+
logger.Printf("[checkNMAgentAPISupport] API %d: %s", i+1, api)
680+
681+
if strings.Contains(api, nmAgentSwiftV2API) { // change
682+
swiftV2Support = true
683+
}
684+
}
685+
686+
logger.Printf("[checkNMAgentAPISupport] Support check - SwiftV2: %t", swiftV2Support)
687+
return swiftV2Support, nil
688+
}
689+
690+
// GetIMDSNCVersions gets NC versions from IMDS and returns them as a map
691+
func (service *HTTPRestService) GetIMDSNCVersions(ctx context.Context) (map[string]string, error) {
692+
logger.Printf("[GetIMDSNCVersions] Getting NC versions from IMDS")
693+
694+
// Check NMAgent API support for SwiftV2, if it fails return empty map assuming support might not be available in that nma build
695+
swiftV2Support, err := service.checkNMAgentAPISupport(ctx)
696+
if err != nil {
697+
logger.Printf("[GetIMDSNCVersions] Failed to check NMAgent API support, returning empty map: %v", err)
698+
return make(map[string]string), nil
699+
}
700+
701+
if !swiftV2Support {
702+
logger.Printf("[GetIMDSNCVersions] SwiftV2 API not supported, returning empty NC versions map")
703+
return make(map[string]string), nil
704+
}
705+
706+
logger.Printf("[GetIMDSNCVersions] SwiftV2 support API exists (%t), proceeding to get NC versions from IMDS", swiftV2Support)
707+
708+
imdsClient := service.imdsClient
709+
710+
// Get all NC versions from IMDS
711+
ncVersions, err := imdsClient.GetNCVersionsFromIMDS(ctx)
712+
if err != nil {
713+
logger.Printf("[GetIMDSNCVersions] Failed to get NC versions from IMDS: %v", err)
714+
return make(map[string]string), nil
715+
}
716+
717+
logger.Printf("[GetIMDSNCVersions] Successfully got %d NC versions from IMDS", len(ncVersions))
718+
return ncVersions, nil
719+
}

0 commit comments

Comments
 (0)