diff --git a/api/pkg/api/handler/instance.go b/api/pkg/api/handler/instance.go index cf5c85a2d..daa94f6c6 100644 --- a/api/pkg/api/handler/instance.go +++ b/api/pkg/api/handler/instance.go @@ -895,6 +895,20 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } } + // Verify that the Machine's capabilities match the Instance Type's capabilities + if machine.InstanceTypeID != nil && machine.ID != "" { + if apiErr := common.VerifyInstanceTypeMachineCapabilitiesMatch(ctx, logger, cih.dbSession, *machine.InstanceTypeID, machine.ID); apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) + } + } + + // Acquire a lock on the MachineID + err = tx.TryAcquireAdvisoryLock(ctx, cdb.GetAdvisoryLockIDFromString(machine.ID), nil) + if err != nil { + logger.Error().Err(err).Msg("Failed to acquire advisory lock on Machine") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to lock Machine: %s for Instance creation. It is likely being considered for another Instance creation request", machine.ID), nil) + } + // Update the machine status to assigned updateInput := cdbm.MachineUpdateInput{ MachineID: machine.ID, @@ -996,14 +1010,10 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { } // Select unallocated Machine for the requested instance type - machine, err = common.GetUnallocatedMachineForInstanceType(ctx, tx, cih.dbSession, instanceType) - if err != nil { - if err == common.ErrInstanceTypeMachineNotFound { - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, - "No Machines are available for specified Instance Type", nil) - } - logger.Error().Err(err).Msg("error retrieving Machine from DB for Instance Type") - return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve available baremetal Machines for specified Instance Type", nil) + var apiErr *cutil.APIError + machine, apiErr = common.GetUnallocatedMachineForInstanceType(ctx, tx, cih.dbSession, *instanceType, logger) + if apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) } } // if apiRequest.InstanceTypeID != nil @@ -2441,6 +2451,16 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { } } + // Verify Instance Type and Machine capabilities match when the request + // changes networking (after interface / VPC validation so more specific + // errors are returned first). Skip for metadata-only updates. + if apiRequest.NeedsCapabilityValidation() && + instance.InstanceTypeID != nil && machine != nil { + if apiErr := common.VerifyInstanceTypeMachineCapabilitiesMatch(ctx, logger, uih.dbSession, *instance.InstanceTypeID, machine.ID); apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) + } + } + mcDAO := cdbm.NewMachineCapabilityDAO(uih.dbSession) // Validate DPU Interfaces if Instance Type has Network Capability with DPU device type diff --git a/api/pkg/api/handler/instance_test.go b/api/pkg/api/handler/instance_test.go index b4f67ef74..d41766a9a 100644 --- a/api/pkg/api/handler/instance_test.go +++ b/api/pkg/api/handler/instance_test.go @@ -364,6 +364,29 @@ func testInstanceBuildMachineInstanceType(t *testing.T, dbSession *cdb.Session, return mit } +func assertMachineInstanceTypeAssociation(t *testing.T, dbSession *cdb.Session, mit *cdbm.MachineInstanceType, mc *cdbm.Machine, in *cdbm.InstanceType) { + t.Helper() + require.NotNil(t, mit) + assert.Equal(t, mc.ID, mit.MachineID) + assert.Equal(t, in.ID, mit.InstanceTypeID) + + mDAO := cdbm.NewMachineDAO(dbSession) + reloaded, err := mDAO.GetByID(context.Background(), nil, mc.ID, nil, false) + require.NoError(t, err) + require.NotNil(t, reloaded) + require.NotNil(t, reloaded.InstanceTypeID) + assert.Equal(t, in.ID, *reloaded.InstanceTypeID) +} + +// testAddMachineCapabilitiesMatchingIST1 adds machine capability rows matching ist1 in +// TestCreateInstanceHandler_Handle (same names/counts as the instance-type capabilities). +func testAddMachineCapabilitiesMatchingIST1(t *testing.T, dbSession *cdb.Session, m *cdbm.Machine) { + t.Helper() + common.TestBuildMachineCapability(t, dbSession, &m.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(3), cdb.GetStrPtr(""), nil) + common.TestBuildMachineCapability(t, dbSession, &m.ID, nil, cdbm.MachineCapabilityTypeNetwork, "MT42822 BlueField-2 integrated ConnectX-6 Dx network controller", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(2), cdb.GetStrPtr("DPU"), nil) + common.TestBuildMachineCapability(t, dbSession, &m.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) +} + func testInstanceBuildMachine(t *testing.T, dbSession *cdb.Session, ip uuid.UUID, site uuid.UUID, isassigned *bool, controllerMachineType *string) *cdbm.Machine { return testInstanceBuildMachineWithID(t, dbSession, ip, site, isassigned, controllerMachineType, "fm"+uuid.NewString()) } @@ -721,7 +744,16 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { mcnoib := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mcnoib) mcinstnoib := testInstanceBuildMachineInstanceType(t, dbSession, mcnoib, istnoib) - assert.NotNil(t, mcinstnoib) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstnoib, mcnoib, istnoib) + + // Instance type with no IT capabilities + machine whose IB capability marks device instance 1 inactive (ValidateInfiniBandInterfaces uses machine IB caps when IT has none) + istInactiveIBOnly := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-ib-inactive-only", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istInactiveIBOnly) + mcInactiveIBCreate := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) + assert.NotNil(t, mcInactiveIBCreate) + mcinstInactiveIBCreate := testInstanceBuildMachineInstanceType(t, dbSession, mcInactiveIBCreate, istInactiveIBOnly) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstInactiveIBCreate, mcInactiveIBCreate, istInactiveIBOnly) + common.TestBuildMachineCapability(t, dbSession, &mcInactiveIBCreate.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(3), cdb.GetStrPtr(""), []int{1}) // machine to instantiate by idbelonging to an instance type istbyid := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-byid", st1, cdbm.InstanceStatusReady) @@ -738,7 +770,29 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { common.TestBuildMachineCapability(t, dbSession, &mcbyid.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) mcinstbyid := testInstanceBuildMachineInstanceType(t, dbSession, mcbyid, istbyid) - assert.NotNil(t, mcinstbyid) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstbyid, mcbyid, istbyid) + + // Instance types + machines for MatchInstanceTypeCapabilitiesForMachines (HTTP 400 / 409 on create) + istCapMismatch := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-cap-mismatch", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istCapMismatch) + common.TestBuildMachineCapability(t, dbSession, nil, &istCapMismatch.ID, cdbm.MachineCapabilityTypeGPU, "GPU-ONLY-ON-IT", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcCapMismatch := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) + assert.NotNil(t, mcCapMismatch) + common.TestBuildMachineCapability(t, dbSession, &mcCapMismatch.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcinstCapMismatch := testInstanceBuildMachineInstanceType(t, dbSession, mcCapMismatch, istCapMismatch) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstCapMismatch, mcCapMismatch, istCapMismatch) + alcISTCapMismatch := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istCapMismatch.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) + assert.NotNil(t, alcISTCapMismatch) + + istCapNoMachineCaps := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-409-no-machine-caps", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istCapNoMachineCaps) + common.TestBuildMachineCapability(t, dbSession, nil, &istCapNoMachineCaps.ID, cdbm.MachineCapabilityTypeGPU, "GPU-REQUIRES-MACHINE-CAPS", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(1), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcCapNoMachineCaps := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) + assert.NotNil(t, mcCapNoMachineCaps) + mcinstCapNoMachineCaps := testInstanceBuildMachineInstanceType(t, dbSession, mcCapNoMachineCaps, istCapNoMachineCaps) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstCapNoMachineCaps, mcCapNoMachineCaps, istCapNoMachineCaps) + alcCapNoMachineCaps := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istCapNoMachineCaps.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) + assert.NotNil(t, alcCapNoMachineCaps) // machine not belonging to an instance type mcnoinst := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) @@ -767,7 +821,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc1) mcinst1 := testInstanceBuildMachineInstanceType(t, dbSession, mc1, ist1) - assert.NotNil(t, mcinst1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst1, mc1, ist1) mc12 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc12) @@ -797,31 +851,35 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc20) mcinst12 := testInstanceBuildMachineInstanceType(t, dbSession, mc12, ist1) - assert.NotNil(t, mcinst12) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst12, mc12, ist1) mcinst13 := testInstanceBuildMachineInstanceType(t, dbSession, mc13, ist1) - assert.NotNil(t, mcinst13) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst13, mc13, ist1) mcinst14 := testInstanceBuildMachineInstanceType(t, dbSession, mc14, ist1) - assert.NotNil(t, mcinst14) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst14, mc14, ist1) mcinst15 := testInstanceBuildMachineInstanceType(t, dbSession, mc15, ist1) - assert.NotNil(t, mcinst15) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst15, mc15, ist1) mcinst16 := testInstanceBuildMachineInstanceType(t, dbSession, mc16, ist1) - assert.NotNil(t, mcinst16) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst16, mc16, ist1) mcinst17 := testInstanceBuildMachineInstanceType(t, dbSession, mc17, ist1) - assert.NotNil(t, mcinst17) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst17, mc17, ist1) mcinst18 := testInstanceBuildMachineInstanceType(t, dbSession, mc18, ist1) - assert.NotNil(t, mcinst18) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst18, mc18, ist1) mcinst19 := testInstanceBuildMachineInstanceType(t, dbSession, mc19, ist1) - assert.NotNil(t, mcinst19) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst19, mc19, ist1) mcinst20 := testInstanceBuildMachineInstanceType(t, dbSession, mc20, ist1) - assert.NotNil(t, mcinst20) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst20, mc20, ist1) + + for _, m := range []*cdbm.Machine{mc1, mc12, mc13, mc14, mc15, mc16, mc17, mc18, mc19, mc20} { + testAddMachineCapabilitiesMatchingIST1(t, dbSession, m) + } // Tenant 1 os1 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-1", tn1, cdbm.OperatingSystemTypeIPXE, false, nil, true, cdbm.OperatingSystemStatusReady, tnu1) @@ -859,6 +917,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { subnet1 := testInstanceBuildSubnet(t, dbSession, "test-subnet-1", tn1, vpc1, cdb.GetUUIDPtr(uuid.New()), cdbm.SubnetStatusReady, tnu1) assert.NotNil(t, subnet1) + testInstanceBuildMachineInterface(t, dbSession, subnet1.ID, mcInactiveIBCreate.ID) subnet2 := testInstanceBuildSubnet(t, dbSession, "test-subnet-2", tn1, vpc2, cdb.GetUUIDPtr(uuid.New()), cdbm.SubnetStatusReady, tnu1) assert.NotNil(t, subnet2) @@ -956,7 +1015,8 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { mc := testInstanceBuildMachine(t, dbSession, ip.ID, st2.ID, cdb.GetBoolPtr(false), nil) ms2 = append(ms2, mc) - testInstanceBuildMachineInstanceType(t, dbSession, mc, ist2) + mcinstLoop := testInstanceBuildMachineInstanceType(t, dbSession, mc, ist2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstLoop, mc, ist2) testInstanceBuildMachineInterface(t, dbSession, subnet3.ID, mc.ID) } @@ -1017,8 +1077,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { mc5 := testInstanceBuildMachine(t, dbSession, ip.ID, st4.ID, cdb.GetBoolPtr(false), nil) mcinst5 := testInstanceBuildMachineInstanceType(t, dbSession, mc5, ist5) - - assert.NotNil(t, mcinst5) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst5, mc5, ist5) os5 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-5", tn5, cdbm.OperatingSystemTypeImage, true, nil, false, cdbm.OperatingSystemStatusReady, tnu5) testInstanceBuildOperatingSystemSiteAssociation(t, dbSession, st4.ID, os5.ID) @@ -1046,7 +1105,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { mc6 := testInstanceBuildMachine(t, dbSession, ip.ID, st6.ID, cdb.GetBoolPtr(false), nil) mcinst6 := testInstanceBuildMachineInstanceType(t, dbSession, mc6, ist6) - assert.NotNil(t, mcinst6) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst6, mc6, ist6) os6 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-6", tn6, cdbm.OperatingSystemTypeIPXE, false, nil, false, cdbm.OperatingSystemStatusReady, tnu6) vpc8 := testInstanceBuildVPC(t, dbSession, "test-vpc-8", ip, tn6, st6, cdb.GetUUIDPtr(uuid.New()), nil, cdb.GetStrPtr(cdbm.VpcEthernetVirtualizer), nil, cdbm.VpcStatusReady, tnu6) @@ -1066,7 +1125,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { testInstanceSiteBuildAllocationContraints(t, dbSession, al10, cdbm.AllocationResourceTypeInstanceType, ist10.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) mc10 := testInstanceBuildMachine(t, dbSession, ip.ID, st7.ID, cdb.GetBoolPtr(false), nil) mcinst10 := testInstanceBuildMachineInstanceType(t, dbSession, mc10, ist10) - assert.NotNil(t, mcinst10) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst10, mc10, ist10) os10 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-10", tn7, cdbm.OperatingSystemTypeIPXE, false, nil, false, cdbm.OperatingSystemStatusReady, tnu7) vpc10 := testInstanceBuildVPC(t, dbSession, "test-vpc-7", ip, tn7, st7, cdb.GetUUIDPtr(uuid.New()), nil, cdb.GetStrPtr(cdbm.VpcEthernetVirtualizer), nil, cdbm.VpcStatusReady, tnu7) subnet10 := testInstanceBuildSubnet(t, dbSession, "test-subnet-10", tn7, vpc10, cdb.GetUUIDPtr(uuid.New()), cdbm.SubnetStatusReady, tnu7) @@ -1078,7 +1137,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { testInstanceSiteBuildAllocationContraints(t, dbSession, al7b, cdbm.AllocationResourceTypeInstanceType, ist7b.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) mc7b := testInstanceBuildMachine(t, dbSession, ip.ID, st7b.ID, cdb.GetBoolPtr(false), nil) mcinst7b := testInstanceBuildMachineInstanceType(t, dbSession, mc7b, ist7b) - assert.NotNil(t, mcinst7b) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst7b, mc7b, ist7b) os7b := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-7b", tn7, cdbm.OperatingSystemTypeIPXE, false, nil, false, cdbm.OperatingSystemStatusReady, tnu7) vpc7b := testInstanceBuildVPC(t, dbSession, "test-vpc-7b", ip, tn7, st7b, cdb.GetUUIDPtr(uuid.New()), nil, cdb.GetStrPtr(cdbm.VpcEthernetVirtualizer), nil, cdbm.VpcStatusReady, tnu7) subnet7b := testInstanceBuildSubnet(t, dbSession, "test-subnet-7b", tn7, vpc7b, cdb.GetUUIDPtr(uuid.New()), cdbm.SubnetStatusReady, tnu7) @@ -1100,7 +1159,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { mc8 := testInstanceBuildMachine(t, dbSession, ip.ID, st8.ID, cdb.GetBoolPtr(false), nil) mcinst8 := testInstanceBuildMachineInstanceType(t, dbSession, mc8, ist8) - assert.NotNil(t, mcinst8) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst8, mc8, ist8) os11 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-11", tn8, cdbm.OperatingSystemTypeImage, true, nil, false, cdbm.OperatingSystemStatusReady, tnu8) ossa2 := testInstanceBuildOperatingSystemSiteAssociation(t, dbSession, st8.ID, os11.ID) @@ -1117,7 +1176,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { testInstanceSiteBuildAllocationContraints(t, dbSession, al8, cdbm.AllocationResourceTypeInstanceType, ist9.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) mc9 := testInstanceBuildMachine(t, dbSession, ip.ID, st8.ID, cdb.GetBoolPtr(false), nil) mcinst9 := testInstanceBuildMachineInstanceType(t, dbSession, mc9, ist9) - assert.NotNil(t, mcinst9) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst9, mc9, ist9) testInstanceBuildMachineInterface(t, dbSession, subnet11.ID, mc9.ID) // FNN VPC @@ -1338,7 +1397,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { }, prepareReq: func(t *testing.T, req *model.APIInstanceCreateRequest) { mc := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) - testInstanceBuildMachineInstanceType(t, dbSession, mc, ist1) + mcinstPrep := testInstanceBuildMachineInstanceType(t, dbSession, mc, ist1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstPrep, mc, ist1) + testAddMachineCapabilitiesMatchingIST1(t, dbSession, mc) testUpdateMachineStatusAndControllerState(t, dbSession, mc, cdbm.MachineStatusError, cdbm.MachineStatusReady) req.MachineID = cdb.GetStrPtr(mc.ID) }, @@ -1374,7 +1435,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { }, prepareReq: func(t *testing.T, req *model.APIInstanceCreateRequest) { mc := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) - testInstanceBuildMachineInstanceType(t, dbSession, mc, ist1) + mcinstPrep := testInstanceBuildMachineInstanceType(t, dbSession, mc, ist1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstPrep, mc, ist1) + testAddMachineCapabilitiesMatchingIST1(t, dbSession, mc) testUpdateMachineStatusAndControllerState(t, dbSession, mc, cdbm.MachineStatusMaintenance, "Offline") req.MachineID = cdb.GetStrPtr(mc.ID) }, @@ -1410,7 +1473,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { }, prepareReq: func(t *testing.T, req *model.APIInstanceCreateRequest) { mc := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) - testInstanceBuildMachineInstanceType(t, dbSession, mc, ist1) + mcinstPrep := testInstanceBuildMachineInstanceType(t, dbSession, mc, ist1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstPrep, mc, ist1) + testAddMachineCapabilitiesMatchingIST1(t, dbSession, mc) testUpdateMachineStatusAndControllerState(t, dbSession, mc, cdbm.MachineStatusInitializing, "Offline") req.MachineID = cdb.GetStrPtr(mc.ID) }, @@ -3106,7 +3171,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { reqMachine: nil, reqOrg: tnOrg3, reqUser: tnu3, - respCode: http.StatusBadRequest, + respCode: http.StatusNotFound, respMessage: "No Machines are available for specified Instance Type", }, wantErr: false, @@ -3346,6 +3411,109 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { wantErr: false, verifyChildSpanner: true, }, + { + name: "test Instance create API endpoint fails when InfiniBand device instance is inactive per machine capability inactiveDevices", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceCreateRequest{ + Name: "Test Instance IB inactive device index", + TenantID: tn1.ID.String(), + MachineID: cdb.GetStrPtr(mcInactiveIBCreate.ID), + VpcID: vpc1.ID.String(), + OperatingSystemID: cdb.GetStrPtr(os1.ID.String()), + UserData: nil, + IpxeScript: cdb.GetStrPtr(common.DefaultIpxeScript), + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{ + { + SubnetID: cdb.GetStrPtr(subnet1.ID.String()), + }, + }, + PhoneHomeEnabled: cdb.GetBoolPtr(false), + InfiniBandInterfaces: []model.APIInfiniBandInterfaceCreateOrUpdateRequest{ + { + InfiniBandPartitionID: ibp1.ID.String(), + Device: "MT28908 Family [ConnectX-6]", + Vendor: cdb.GetStrPtr("Mellanox Technologies"), + DeviceInstance: 1, + IsPhysical: true, + }, + }, + }, + reqMachine: mcInactiveIBCreate, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusBadRequest, + respMessage: "Device Instance: 1 for Device MT28908 Family [ConnectX-6] is inactive", + }, + wantErr: false, + verifyChildSpanner: true, + }, + { + name: "test Instance create API endpoint fails when machine capabilities do not match instance type capabilities", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceCreateRequest{ + Name: "Test Instance capability mismatch", + TenantID: tn1.ID.String(), + MachineID: cdb.GetStrPtr(mcCapMismatch.ID), + VpcID: vpc1.ID.String(), + UserData: cdb.GetStrPtr(""), + IpxeScript: cdb.GetStrPtr(common.DefaultIpxeScript), + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{ + { + SubnetID: cdb.GetStrPtr(subnet1.ID.String()), + }, + }, + PhoneHomeEnabled: cdb.GetBoolPtr(false), + }, + reqMachine: mcCapMismatch, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusBadRequest, + respMessage: fmt.Sprintf("Capabilities for Machine: %s do not match Instance Type's Capabilities", mcCapMismatch.ID), + }, + wantErr: false, + verifyChildSpanner: true, + }, + { + name: "test Instance create API endpoint fails when machine has no capabilities to match instance type", + fields: fields{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceCreateRequest{ + Name: "Test Instance no machine capabilities", + TenantID: tn1.ID.String(), + MachineID: cdb.GetStrPtr(mcCapNoMachineCaps.ID), + VpcID: vpc1.ID.String(), + UserData: cdb.GetStrPtr(""), + IpxeScript: cdb.GetStrPtr(common.DefaultIpxeScript), + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{ + { + SubnetID: cdb.GetStrPtr(subnet1.ID.String()), + }, + }, + PhoneHomeEnabled: cdb.GetBoolPtr(false), + }, + reqMachine: mcCapNoMachineCaps, + reqOrg: tnOrg, + reqUser: tnu1, + respCode: http.StatusConflict, + respMessage: "Machines specified in request currently do not have any Capabilities to match against Instance Type", + }, + wantErr: false, + verifyChildSpanner: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -3602,13 +3770,13 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc4) mcinst1 := testInstanceBuildMachineInstanceType(t, dbSession, mc1, ist1) - assert.NotNil(t, mcinst1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst1, mc1, ist1) mcinst2 := testInstanceBuildMachineInstanceType(t, dbSession, mc3, ist2) - assert.NotNil(t, mcinst2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst2, mc3, ist2) mcinst4 := testInstanceBuildMachineInstanceType(t, dbSession, mc4, ist1) - assert.NotNil(t, mcinst4) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst4, mc4, ist1) // Build SSHKeyGroup 1 skg1 := testBuildSSHKeyGroup(t, dbSession, "test-sshkeygroup-1", tnOrg1, cdb.GetStrPtr("test"), tn1.ID, cdb.GetStrPtr("12345"), cdbm.SSHKeyGroupStatusSynced, tnu1.ID) @@ -3747,7 +3915,7 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc5) mcinst5 := testInstanceBuildMachineInstanceType(t, dbSession, mc5, ist4) - assert.NotNil(t, mcinst5) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst5, mc5, ist4) al3 := testInstanceSiteBuildAllocation(t, dbSession, st3, tn2, "test-allocation-3", ipu) assert.NotNil(t, al3) @@ -3803,13 +3971,16 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { mc7 := testInstanceBuildMachine(t, dbSession, ip.ID, st3.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc7) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc7, ist4)) + mcinst7 := testInstanceBuildMachineInstanceType(t, dbSession, mc7, ist4) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst7, mc7, ist4) mc8 := testInstanceBuildMachine(t, dbSession, ip.ID, st3.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc8) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc8, ist4)) + mcinst8u := testInstanceBuildMachineInstanceType(t, dbSession, mc8, ist4) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst8u, mc8, ist4) mc9 := testInstanceBuildMachine(t, dbSession, ip.ID, st3.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc9) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc9, ist4)) + mcinst9u := testInstanceBuildMachineInstanceType(t, dbSession, mc9, ist4) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst9u, mc9, ist4) buildNsgPropagationMultiVpcPair := func(primaryName, secondaryName, primaryPrefixName, secondaryPrefixName, primaryCIDR, secondaryCIDR string) (*cdbm.Vpc, *cdbm.Vpc, *cdbm.VpcPrefix, *cdbm.VpcPrefix) { primary := testInstanceBuildVPC(t, dbSession, primaryName, ip, tn1, st3, nil, nil, cdb.GetStrPtr(cdbm.VpcFNN), nil, cdbm.VpcStatusReady, tnu1) @@ -3866,11 +4037,21 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { // Add Network DPU capability to Instance Type common.TestBuildMachineCapability(t, dbSession, nil, &ist4.ID, cdbm.MachineCapabilityTypeNetwork, "MT42822 BlueField-2 integrated ConnectX-6 Dx network controller", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(2), cdb.GetStrPtr("DPU"), nil) + // NVLink GPU on ist4 so machines with Network+DPU+GPU match the instance type (e.g. mc5 after GPU is added below) + common.TestBuildMachineCapability(t, dbSession, nil, &ist4.ID, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + // Same capability on machines paired with ist4 so VerifyInstanceTypeMachineCapabilitiesMatch succeeds on interface updates + common.TestBuildMachineCapability(t, dbSession, &mc5.ID, nil, cdbm.MachineCapabilityTypeNetwork, "MT42822 BlueField-2 integrated ConnectX-6 Dx network controller", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(2), cdb.GetStrPtr("DPU"), nil) + common.TestBuildMachineCapability(t, dbSession, &mc7.ID, nil, cdbm.MachineCapabilityTypeNetwork, "MT42822 BlueField-2 integrated ConnectX-6 Dx network controller", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(2), cdb.GetStrPtr("DPU"), nil) + common.TestBuildMachineCapability(t, dbSession, &mc8.ID, nil, cdbm.MachineCapabilityTypeNetwork, "MT42822 BlueField-2 integrated ConnectX-6 Dx network controller", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(2), cdb.GetStrPtr("DPU"), nil) + common.TestBuildMachineCapability(t, dbSession, &mc9.ID, nil, cdbm.MachineCapabilityTypeNetwork, "MT42822 BlueField-2 integrated ConnectX-6 Dx network controller", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(2), cdb.GetStrPtr("DPU"), nil) inst13 := testInstanceBuildInstance(t, dbSession, "test-instance-nvlink-update", tn1.ID, ip.ID, st3.ID, &ist4.ID, vpc4.ID, cdb.GetStrPtr(mc5.ID), &os2.ID, nil, cdbm.InstanceStatusReady) // Add NVLink GPU capability to Machine common.TestBuildMachineCapability(t, dbSession, &mc5.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + common.TestBuildMachineCapability(t, dbSession, &mc7.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + common.TestBuildMachineCapability(t, dbSession, &mc8.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + common.TestBuildMachineCapability(t, dbSession, &mc9.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) nvllp1 := testBuildNVLinkLogicalPartition(t, dbSession, "test-nvllp-1", cdb.GetStrPtr("Test NVLink Logical Partition"), tnOrg1, st3, tn1, cdb.GetStrPtr(cdbm.NVLinkLogicalPartitionStatusReady), false) assert.NotNil(t, nvllp1) @@ -3894,7 +4075,7 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc6) mcinst6 := testInstanceBuildMachineInstanceType(t, dbSession, mc6, ist2) - assert.NotNil(t, mcinst6) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst6, mc6, ist2) inst14 := testInstanceBuildInstance(t, dbSession, "test-instance-14", tn2.ID, ip.ID, st2.ID, &ist2.ID, vpc2.ID, cdb.GetStrPtr(mc6.ID), &os4.ID, nil, cdbm.InstanceStatusError) assert.NotNil(t, inst14) @@ -3943,6 +4124,10 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { // Add InfiniBand capability to Instance Type common.TestBuildMachineCapability(t, dbSession, nil, &ist1.ID, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) + // Same capability on machines paired with ist1 so VerifyInstanceTypeMachineCapabilitiesMatch succeeds on interface / IB updates + common.TestBuildMachineCapability(t, dbSession, &mc1.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) + common.TestBuildMachineCapability(t, dbSession, &mc2.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) + common.TestBuildMachineCapability(t, dbSession, &mc4.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) ibp2 := testBuildIBPartition(t, dbSession, "test-ibp-2", tnOrg1, st1, tn1, cdb.GetUUIDPtr(uuid.New()), cdb.GetStrPtr(cdbm.InfiniBandPartitionStatusReady), false) assert.NotNil(t, ibp2) @@ -4006,6 +4191,51 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { desd17 := common.TestBuildDpuExtensionServiceDeployment(t, dbSession, des1, inst17.ID, "1.0.0", cdbm.DpuExtensionServiceDeploymentStatusRunning, tnu1) assert.NotNil(t, desd17) + common.TestBuildMachineCapability(t, dbSession, &mc15.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) + common.TestBuildMachineCapability(t, dbSession, &mc16.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) + common.TestBuildMachineCapability(t, dbSession, &mc17.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(5), cdb.GetStrPtr(""), nil) + + // Instance types + instances for MatchInstanceTypeCapabilitiesForMachines on update (HTTP 400 / 409) + istUpdateCapMismatch := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-update-cap-mismatch", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istUpdateCapMismatch) + common.TestBuildMachineCapability(t, dbSession, nil, &istUpdateCapMismatch.ID, cdbm.MachineCapabilityTypeGPU, "GPU-UPDATE-ONLY-ON-IT", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcUpdateCapMismatch := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) + assert.NotNil(t, mcUpdateCapMismatch) + common.TestBuildMachineCapability(t, dbSession, &mcUpdateCapMismatch.ID, nil, cdbm.MachineCapabilityTypeGPU, "NVIDIA GB200", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcinstUpdateCapMismatch := testInstanceBuildMachineInstanceType(t, dbSession, mcUpdateCapMismatch, istUpdateCapMismatch) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstUpdateCapMismatch, mcUpdateCapMismatch, istUpdateCapMismatch) + alcUpdateCapMismatch := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istUpdateCapMismatch.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) + assert.NotNil(t, alcUpdateCapMismatch) + testInstanceBuildMachineInterface(t, dbSession, subnet1.ID, mcUpdateCapMismatch.ID) + instUpdateCapMismatch := testInstanceBuildInstance(t, dbSession, "test-instance-update-cap-mismatch", al1.ID, alcUpdateCapMismatch.ID, tn1.ID, ip.ID, st1.ID, &istUpdateCapMismatch.ID, vpc1.ID, cdb.GetStrPtr(mcUpdateCapMismatch.ID), &os2.ID, nil, cdbm.InstanceStatusReady) + assert.NotNil(t, instUpdateCapMismatch) + + istUpdateNoMachineCaps := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-update-no-machine-caps", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istUpdateNoMachineCaps) + common.TestBuildMachineCapability(t, dbSession, nil, &istUpdateNoMachineCaps.ID, cdbm.MachineCapabilityTypeGPU, "GPU-UPDATE-REQUIRES-MACHINE-CAPS", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(1), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcUpdateNoMachineCaps := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) + assert.NotNil(t, mcUpdateNoMachineCaps) + mcinstUpdateNoMachineCaps := testInstanceBuildMachineInstanceType(t, dbSession, mcUpdateNoMachineCaps, istUpdateNoMachineCaps) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstUpdateNoMachineCaps, mcUpdateNoMachineCaps, istUpdateNoMachineCaps) + alcUpdateNoMachineCaps := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istUpdateNoMachineCaps.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) + assert.NotNil(t, alcUpdateNoMachineCaps) + testInstanceBuildMachineInterface(t, dbSession, subnet1.ID, mcUpdateNoMachineCaps.ID) + instUpdateNoMachineCaps := testInstanceBuildInstance(t, dbSession, "test-instance-update-no-machine-caps", al1.ID, alcUpdateNoMachineCaps.ID, tn1.ID, ip.ID, st1.ID, &istUpdateNoMachineCaps.ID, vpc1.ID, cdb.GetStrPtr(mcUpdateNoMachineCaps.ID), &os2.ID, nil, cdbm.InstanceStatusReady) + assert.NotNil(t, instUpdateNoMachineCaps) + + istUpdateInactiveIBOnly := testInstanceBuildInstanceType(t, dbSession, ip, "test-instance-type-update-ib-inactive-only", st1, cdbm.InstanceStatusReady) + assert.NotNil(t, istUpdateInactiveIBOnly) + alcUpdateInactiveIB := testInstanceSiteBuildAllocationContraints(t, dbSession, al1, cdbm.AllocationResourceTypeInstanceType, istUpdateInactiveIBOnly.ID, cdbm.AllocationConstraintTypeReserved, 1, ipu) + assert.NotNil(t, alcUpdateInactiveIB) + mcUpdateInactiveIB := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) + assert.NotNil(t, mcUpdateInactiveIB) + mcinstUpdateInactiveIB := testInstanceBuildMachineInstanceType(t, dbSession, mcUpdateInactiveIB, istUpdateInactiveIBOnly) + assertMachineInstanceTypeAssociation(t, dbSession, mcinstUpdateInactiveIB, mcUpdateInactiveIB, istUpdateInactiveIBOnly) + common.TestBuildMachineCapability(t, dbSession, &mcUpdateInactiveIB.ID, nil, cdbm.MachineCapabilityTypeInfiniBand, "MT28908 Family [ConnectX-6]", nil, nil, cdb.GetStrPtr("Mellanox Technologies"), cdb.GetIntPtr(3), cdb.GetStrPtr(""), []int{1}) + testInstanceBuildMachineInterface(t, dbSession, subnet1.ID, mcUpdateInactiveIB.ID) + instUpdateInactiveIB := testInstanceBuildInstance(t, dbSession, "test-instance-update-ib-inactive", al1.ID, alcUpdateInactiveIB.ID, tn1.ID, ip.ID, st1.ID, &istUpdateInactiveIBOnly.ID, vpc1.ID, cdb.GetStrPtr(mcUpdateInactiveIB.ID), &os2.ID, nil, cdbm.InstanceStatusReady) + assert.NotNil(t, instUpdateInactiveIB) + e := echo.New() cfg := common.GetTestConfig() tc := &tmocks.Client{} @@ -4085,8 +4315,25 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { expectedNetworkSecurityGroupInherited *bool expectedPropagationDetailedStatus *string expectedPropagationStatus *string + // reqBodyJSON, if set, is sent as the PATCH body instead of json.Marshal(reqData). + // Used where the JSON must include explicit keys (e.g. "interfaces":[]) so Echo + // binds a non-nil empty slice and IsInterfaceUpdateRequest() is true. + reqBodyJSON []byte } + capMismatchUpdateBody, err := json.Marshal(map[string]interface{}{ + "name": "Test Instance capability mismatch update", + "ipxeScript": *os2.IpxeScript, + "interfaces": []interface{}{}, + }) + require.NoError(t, err) + noMachineCapsUpdateBody, err := json.Marshal(map[string]interface{}{ + "name": "Test Instance no machine capabilities update", + "ipxeScript": *os2.IpxeScript, + "interfaces": []interface{}{}, + }) + require.NoError(t, err) + tests := []struct { name string fields fields @@ -5732,6 +5979,83 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { }, wantErr: false, }, + { + name: "test Instance update API endpoint fails when machine capabilities do not match instance type capabilities", + fields: fields{ + dbSession: dbSession, + tc: tc, + scp: scp, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceUpdateRequest{ + Name: cdb.GetStrPtr("Test Instance capability mismatch update"), + IpxeScript: os2.IpxeScript, + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{}, + }, + reqBodyJSON: capMismatchUpdateBody, + reqInstance: instUpdateCapMismatch.ID.String(), + reqOrg: tnOrg1, + reqUser: tnu1, + respCode: http.StatusBadRequest, + respMessage: cdb.GetStrPtr(fmt.Sprintf("Capabilities for Machine: %s do not match Instance Type's Capabilities", mcUpdateCapMismatch.ID)), + }, + wantErr: false, + }, + { + name: "test Instance update API endpoint fails when machine has no capabilities to match instance type", + fields: fields{ + dbSession: dbSession, + tc: tc, + scp: scp, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceUpdateRequest{ + Name: cdb.GetStrPtr("Test Instance no machine capabilities update"), + IpxeScript: os2.IpxeScript, + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{}, + }, + reqBodyJSON: noMachineCapsUpdateBody, + reqInstance: instUpdateNoMachineCaps.ID.String(), + reqOrg: tnOrg1, + reqUser: tnu1, + respCode: http.StatusConflict, + respMessage: cdb.GetStrPtr("Machines specified in request currently do not have any Capabilities to match against Instance Type"), + }, + wantErr: false, + }, + { + name: "test Instance update API endpoint fails when InfiniBand device instance is inactive per machine capability inactiveDevices", + fields: fields{ + dbSession: dbSession, + tc: tc, + scp: scp, + cfg: cfg, + }, + args: args{ + reqData: &model.APIInstanceUpdateRequest{ + Name: cdb.GetStrPtr("Test Instance update IB inactive device index"), + IpxeScript: os2.IpxeScript, + Interfaces: []model.APIInterfaceCreateOrUpdateRequest{}, + InfiniBandInterfaces: []model.APIInfiniBandInterfaceCreateOrUpdateRequest{ + { + InfiniBandPartitionID: ibp1.ID.String(), + Device: "MT28908 Family [ConnectX-6]", + Vendor: cdb.GetStrPtr("Mellanox Technologies"), + DeviceInstance: 1, + IsPhysical: true, + }, + }, + }, + reqInstance: instUpdateInactiveIB.ID.String(), + reqOrg: tnOrg1, + reqUser: tnu1, + respCode: http.StatusBadRequest, + respMessage: cdb.GetStrPtr("Device Instance: 1 for Device MT28908 Family [ConnectX-6] is inactive"), + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -5742,7 +6066,13 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { cfg: tt.fields.cfg, } - jsonData, _ := json.Marshal(tt.args.reqData) + var jsonData []byte + if tt.args.reqBodyJSON != nil { + jsonData = tt.args.reqBodyJSON + } else { + jsonData, err = json.Marshal(tt.args.reqData) + require.NoError(t, err) + } // Setup echo server/context req := httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(jsonData))) @@ -6183,19 +6513,19 @@ func TestGetInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc1) mcinst1 := testInstanceBuildMachineInstanceType(t, dbSession, mc1, ist1) - assert.NotNil(t, mcinst1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst1, mc1, ist1) mc2 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc2) mcinst2 := testInstanceBuildMachineInstanceType(t, dbSession, mc2, ist1) - assert.NotNil(t, mcinst2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst2, mc2, ist1) mc3 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc3) mcinst3 := testInstanceBuildMachineInstanceType(t, dbSession, mc3, ist1) - assert.NotNil(t, mcinst3) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst3, mc3, ist1) os1 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-1", tn1, cdbm.OperatingSystemTypeImage, false, nil, false, cdbm.OperatingSystemStatusReady, tnu1) assert.NotNil(t, os1) @@ -6258,13 +6588,16 @@ func TestGetInstanceHandler_Handle(t *testing.T) { mc4 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc4) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc4, ist1)) + mcinst4g := testInstanceBuildMachineInstanceType(t, dbSession, mc4, ist1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst4g, mc4, ist1) mc5 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc5) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc5, ist1)) + mcinst5g := testInstanceBuildMachineInstanceType(t, dbSession, mc5, ist1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst5g, mc5, ist1) mc6 := testInstanceBuildMachine(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil) assert.NotNil(t, mc6) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc6, ist1)) + mcinst6g := testInstanceBuildMachineInstanceType(t, dbSession, mc6, ist1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst6g, mc6, ist1) buildMultiVpcPair := func(primaryName, secondaryName, primaryPrefixName, secondaryPrefixName, primaryCIDR, secondaryCIDR string) (*cdbm.Vpc, *cdbm.Vpc, *cdbm.VpcPrefix, *cdbm.VpcPrefix) { primary := testInstanceBuildVPC(t, dbSession, primaryName, ip, tn1, st1, cdb.GetUUIDPtr(uuid.New()), nil, cdb.GetStrPtr(cdbm.VpcFNN), nil, cdbm.VpcStatusReady, tnu1) @@ -6862,11 +7195,11 @@ func TestGetAllInstanceHandler_Handle(t *testing.T) { mc3 := testInstanceBuildMachineWithID(t, dbSession, ip.ID, st3.ID, cdb.GetBoolPtr(false), nil, "machine-3") assert.NotNil(t, mc3) mcinst1 := testInstanceBuildMachineInstanceType(t, dbSession, mc1, ist1) - assert.NotNil(t, mcinst1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst1, mc1, ist1) mcinst2 := testInstanceBuildMachineInstanceType(t, dbSession, mc2, ist2) - assert.NotNil(t, mcinst2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst2, mc2, ist2) mcinst3 := testInstanceBuildMachineInstanceType(t, dbSession, mc3, ist4) - assert.NotNil(t, mcinst3) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst3, mc3, ist4) os1 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-1", tn1, cdbm.OperatingSystemTypeImage, false, nil, false, cdbm.OperatingSystemStatusReady, tnu1) assert.NotNil(t, os1) @@ -6954,13 +7287,16 @@ func TestGetAllInstanceHandler_Handle(t *testing.T) { mc4 := testInstanceBuildMachineWithID(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil, "machine-getall-4") assert.NotNil(t, mc4) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc4, ist2)) + mcinst4ga := testInstanceBuildMachineInstanceType(t, dbSession, mc4, ist2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst4ga, mc4, ist2) mc5 := testInstanceBuildMachineWithID(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil, "machine-getall-5") assert.NotNil(t, mc5) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc5, ist2)) + mcinst5ga := testInstanceBuildMachineInstanceType(t, dbSession, mc5, ist2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst5ga, mc5, ist2) mc6 := testInstanceBuildMachineWithID(t, dbSession, ip.ID, st1.ID, cdb.GetBoolPtr(false), nil, "machine-getall-6") assert.NotNil(t, mc6) - assert.NotNil(t, testInstanceBuildMachineInstanceType(t, dbSession, mc6, ist2)) + mcinst6ga := testInstanceBuildMachineInstanceType(t, dbSession, mc6, ist2) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst6ga, mc6, ist2) buildMultiVpcPair := func(primaryName, secondaryName, primaryPrefixName, secondaryPrefixName, primaryCIDR, secondaryCIDR string) (*cdbm.Vpc, *cdbm.Vpc, *cdbm.VpcPrefix, *cdbm.VpcPrefix) { primary := testInstanceBuildVPC(t, dbSession, primaryName, ip, tn1, st1, cdb.GetUUIDPtr(uuid.New()), nil, cdb.GetStrPtr(cdbm.VpcFNN), nil, cdbm.VpcStatusReady, tnu1) @@ -8395,7 +8731,7 @@ func TestDeleteInstanceHandler_Handle(t *testing.T) { assert.NotNil(t, mc1) mcinst1 := testInstanceBuildMachineInstanceType(t, dbSession, mc1, ist1) - assert.NotNil(t, mcinst1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst1, mc1, ist1) os1 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-1", tn1, cdbm.OperatingSystemTypeImage, false, nil, false, cdbm.OperatingSystemStatusReady, tnu1) assert.NotNil(t, os1) @@ -9033,7 +9369,7 @@ func TestInstanceHandler_GetStatusDetails(t *testing.T) { assert.NotNil(t, mc1) mcinst1 := testInstanceBuildMachineInstanceType(t, dbSession, mc1, ist1) - assert.NotNil(t, mcinst1) + assertMachineInstanceTypeAssociation(t, dbSession, mcinst1, mc1, ist1) os1 := testInstanceBuildOperatingSystem(t, dbSession, "test-operating-system-1", tn1, cdbm.OperatingSystemTypeImage, false, nil, false, cdbm.OperatingSystemStatusReady, tnu1) assert.NotNil(t, os1) diff --git a/api/pkg/api/handler/util/common/common.go b/api/pkg/api/handler/util/common/common.go index ab1caa855..8ae3e2076 100644 --- a/api/pkg/api/handler/util/common/common.go +++ b/api/pkg/api/handler/util/common/common.go @@ -315,14 +315,10 @@ func AcquireInstanceTypeQuotaLock(ctx context.Context, tx *cdb.Tx, tenantID uuid } // GetUnallocatedMachineForInstanceType provides unallocatd machine based on instancetype -func GetUnallocatedMachineForInstanceType(ctx context.Context, tx *cdb.Tx, dbSession *cdb.Session, instancetype *cdbm.InstanceType) (*cdbm.Machine, error) { - if instancetype == nil { - return nil, ErrInvalidFunctionParams - } - +func GetUnallocatedMachineForInstanceType(ctx context.Context, tx *cdb.Tx, dbSession *cdb.Session, instanceType cdbm.InstanceType, logger zerolog.Logger) (*cdbm.Machine, *cutil.APIError) { // tx has to be set, required acquring lock if tx == nil { - return nil, ErrInvalidFunctionParams + return nil, cutil.NewAPIError(http.StatusInternalServerError, "Failed to retrieve available Machines, transaction required for Machine selection", nil) } mcDAO := cdbm.NewMachineDAO(dbSession) @@ -330,14 +326,15 @@ func GetUnallocatedMachineForInstanceType(ctx context.Context, tx *cdb.Tx, dbSes // Get all available Machines for the Instance Type // Since this query is occurring outside of a lock, we will have to double check availability of Machines filterInput := cdbm.MachineFilterInput{ - SiteID: instancetype.SiteID, - InstanceTypeIDs: []uuid.UUID{instancetype.ID}, + SiteID: instanceType.SiteID, + InstanceTypeIDs: []uuid.UUID{instanceType.ID}, IsAssigned: cdb.GetBoolPtr(false), Statuses: []string{cdbm.MachineStatusReady}, + ExcludeMetadata: true, } machines, _, err := mcDAO.GetAll(ctx, tx, filterInput, cdbp.PageInput{Limit: cdb.GetIntPtr(cdbp.TotalLimit)}, nil) if err != nil { - return nil, err + return nil, cutil.NewAPIError(http.StatusInternalServerError, "Failed to retrieve available Machines for Instance Type, DB error", nil) } // Randomize the list of machines. @@ -354,6 +351,8 @@ func GetUnallocatedMachineForInstanceType(ctx context.Context, tx *cdb.Tx, dbSes }, ) + capabilityMismatchCount := 0 + if len(machines) > 0 { for _, mc := range machines { // Acquire an advisory lock on the MachineID, other provider will be look for other is this is being locked @@ -377,12 +376,19 @@ func GetUnallocatedMachineForInstanceType(ctx context.Context, tx *cdb.Tx, dbSes continue } + // Verify that the Machine's capabilities match the Instance Type's capabilities. + if apiErr := VerifyInstanceTypeMachineCapabilitiesMatch(ctx, logger, dbSession, instanceType.ID, mc.ID); apiErr != nil { + capabilityMismatchCount++ + continue + } + // We should now be able to proceed with the allocation // Update the machine status to assigned updateInput := cdbm.MachineUpdateInput{ MachineID: mc.ID, IsAssigned: cdb.GetBoolPtr(true), } + // return the updated machine mcu, err := mcDAO.Update(ctx, tx, updateInput) if err != nil { @@ -391,7 +397,12 @@ func GetUnallocatedMachineForInstanceType(ctx context.Context, tx *cdb.Tx, dbSes return mcu, nil } } - return nil, ErrInstanceTypeMachineNotFound + + if capabilityMismatchCount > 0 { + return nil, cutil.NewAPIError(http.StatusNotAcceptable, fmt.Sprintf("%d Machines for this Instance Type were found with capability mismatch, unable to select a Machine for provisioning", capabilityMismatchCount), nil) + } + + return nil, cutil.NewAPIError(http.StatusNotFound, "No Machines are available for specified Instance Type", nil) } // GetCountOfMachinesForInstanceType is a utility function to return count of @@ -960,6 +971,20 @@ func MatchInstanceTypeCapabilitiesForMachines(ctx context.Context, logger zerolo return true, nil, nil } +// VerifyInstanceTypeMachineCapabilitiesMatch validates that the Machine's +// capabilities satisfy the Instance Type's capabilities. It returns a non-nil +// *cutil.APIError when validation fails, or nil on success. +func VerifyInstanceTypeMachineCapabilitiesMatch(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session, instanceTypeID uuid.UUID, machineID string) *cutil.APIError { + isMatch, _, apiErr := MatchInstanceTypeCapabilitiesForMachines(ctx, logger, dbSession, instanceTypeID, []string{machineID}) + if apiErr != nil { + return apiErr + } + if !isMatch { + return cutil.NewAPIError(http.StatusBadRequest, fmt.Sprintf("Capabilities for Machine: %v do not match Instance Type's Capabilities", machineID), nil) + } + return nil +} + // GetAllocationResourceTypeMaps is a utility function to get resource info based on resource type in allocation constraints // currently its only supports Instance Type and IPBlock func GetAllocationResourceTypeMaps(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session, acs []cdbm.AllocationConstraint) ( diff --git a/api/pkg/api/handler/util/common/common_test.go b/api/pkg/api/handler/util/common/common_test.go index 137deba8a..d2cfca0b0 100644 --- a/api/pkg/api/handler/util/common/common_test.go +++ b/api/pkg/api/handler/util/common/common_test.go @@ -849,6 +849,7 @@ func TestGetAllocationConstraintsForInstanceType(t *testing.T) { func TestGetUnallocatedMachineForInstanceType(t *testing.T) { ctx := context.Background() + logger := zerolog.New(os.Stdout).Level(zerolog.DebugLevel).With().Timestamp().Logger() dbSession := testCommonInitDB(t) defer dbSession.Close() @@ -895,10 +896,37 @@ func TestGetUnallocatedMachineForInstanceType(t *testing.T) { assert.NotNil(t, mit) } + // Instance types + machines for VerifyInstanceTypeMachineCapabilitiesMatch in GetUnallocatedMachineForInstanceType + // (skip machines that fail the check while more candidates exist; if every candidate mismatches, return 406 with an aggregate message). + instCapPair := testCommonBuildInstanceType(t, dbSession, "it-cap-pair", site1, ip, tnuser) + TestBuildMachineCapability(t, dbSession, nil, &instCapPair.ID, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-UNALLOC", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcCapBad := testCommonBuildMachine(t, dbSession, ip.ID, site1.ID, cdb.GetUUIDPtr(instCapPair.ID), uuid.New(), nil, nil, nil, cdbm.MachineStatusReady) + testCommonBuildMachineInstanceType(t, dbSession, mcCapBad.ID, instCapPair.ID) + TestBuildMachineCapability(t, dbSession, &mcCapBad.ID, nil, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-UNALLOC", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(2), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcCapGood := testCommonBuildMachine(t, dbSession, ip.ID, site1.ID, cdb.GetUUIDPtr(instCapPair.ID), uuid.New(), nil, nil, nil, cdbm.MachineStatusReady) + testCommonBuildMachineInstanceType(t, dbSession, mcCapGood.ID, instCapPair.ID) + TestBuildMachineCapability(t, dbSession, &mcCapGood.ID, nil, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-UNALLOC", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + + instCapSingleBad := testCommonBuildInstanceType(t, dbSession, "it-cap-single-bad", site1, ip, tnuser) + TestBuildMachineCapability(t, dbSession, nil, &instCapSingleBad.ID, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-SINGLE", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + mcCapOnlyBad := testCommonBuildMachine(t, dbSession, ip.ID, site1.ID, cdb.GetUUIDPtr(instCapSingleBad.ID), uuid.New(), nil, nil, nil, cdbm.MachineStatusReady) + testCommonBuildMachineInstanceType(t, dbSession, mcCapOnlyBad.ID, instCapSingleBad.ID) + TestBuildMachineCapability(t, dbSession, &mcCapOnlyBad.ID, nil, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-SINGLE", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(1), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + + instCapTwoBad := testCommonBuildInstanceType(t, dbSession, "it-cap-two-bad", site1, ip, tnuser) + TestBuildMachineCapability(t, dbSession, nil, &instCapTwoBad.ID, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-TWO", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(4), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + for range 2 { + mcTwoBad := testCommonBuildMachine(t, dbSession, ip.ID, site1.ID, cdb.GetUUIDPtr(instCapTwoBad.ID), uuid.New(), nil, nil, nil, cdbm.MachineStatusReady) + testCommonBuildMachineInstanceType(t, dbSession, mcTwoBad.ID, instCapTwoBad.ID) + TestBuildMachineCapability(t, dbSession, &mcTwoBad.ID, nil, cdbm.MachineCapabilityTypeGPU, "GPU-CAP-TWO", nil, nil, cdb.GetStrPtr("NVIDIA"), cdb.GetIntPtr(2), cdb.GetStrPtr(cdbm.MachineCapabilityDeviceTypeNVLink), nil) + } + tests := []struct { - name string - instancetype *cdbm.InstanceType - expectErr bool + name string + instancetype *cdbm.InstanceType + expectErr bool + expectHTTPStatus int + apiErrMsgContains string }{ { name: "success when machine and machine instance type exists", @@ -906,16 +934,50 @@ func TestGetUnallocatedMachineForInstanceType(t *testing.T) { expectErr: false, }, { - name: "error when allocation does not exist", - instancetype: nil, - expectErr: true, + name: "error when instance type is empty", + instancetype: nil, + expectErr: true, + expectHTTPStatus: http.StatusNotFound, + apiErrMsgContains: "No Machines are available for specified Instance Type", + }, + { + name: "success when one machine fails capability match but another machine matches", + instancetype: instCapPair, + expectErr: false, + }, + { + name: "NotAcceptable when only candidate machine fails capability match", + instancetype: instCapSingleBad, + expectErr: true, + expectHTTPStatus: http.StatusNotAcceptable, + apiErrMsgContains: "1 Machines for this Instance Type were found with capability mismatch", + }, + { + name: "NotAcceptable when every candidate machine fails capability match", + instancetype: instCapTwoBad, + expectErr: true, + expectHTTPStatus: http.StatusNotAcceptable, + apiErrMsgContains: "2 Machines for this Instance Type were found with capability mismatch", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - s, err := GetUnallocatedMachineForInstanceType(ctx, tx, dbSession, tc.instancetype) - assert.Equal(t, tc.expectErr, err != nil) - if err == nil { + var it cdbm.InstanceType + if tc.instancetype != nil { + it = *tc.instancetype + } + s, apiErr := GetUnallocatedMachineForInstanceType(ctx, tx, dbSession, it, logger) + if tc.expectErr { + assert.NotNil(t, apiErr) + assert.Nil(t, s) + if tc.expectHTTPStatus != 0 { + assert.Equal(t, tc.expectHTTPStatus, apiErr.Code) + } + if tc.apiErrMsgContains != "" { + assert.Contains(t, apiErr.Message, tc.apiErrMsgContains) + } + } else { + assert.Nil(t, apiErr) assert.NotNil(t, s) } }) diff --git a/api/pkg/api/model/instance.go b/api/pkg/api/model/instance.go index 2a60f189c..51cabc651 100644 --- a/api/pkg/api/model/instance.go +++ b/api/pkg/api/model/instance.go @@ -1372,6 +1372,23 @@ func (iur *APIInstanceUpdateRequest) IsInterfaceUpdateRequest() bool { return iur.Interfaces != nil || iur.InfiniBandInterfaces != nil || iur.NVLinkInterfaces != nil } +// NeedsCapabilityValidation returns true when the update touches interface or +// secondary-VPC networking so Instance Type vs Machine capabilities must be validated. +// Pure metadata/OS/NSG/SSH updates return false so existing instances without +// machine capability rows still work. +func (iur *APIInstanceUpdateRequest) NeedsCapabilityValidation() bool { + if iur == nil { + return false + } + if iur.IsInterfaceUpdateRequest() { + return true + } + if iur.SecondaryVpcIDs != nil { + return true + } + return false +} + // IsRebootRequest checks if the request is an instance reboot request func (iur *APIInstanceUpdateRequest) IsRebootRequest() bool { return iur.TriggerReboot != nil && *iur.TriggerReboot