diff --git a/api/pkg/api/handler/instance.go b/api/pkg/api/handler/instance.go index faa5aed31..6cc4e8e7b 100644 --- a/api/pkg/api/handler/instance.go +++ b/api/pkg/api/handler/instance.go @@ -584,6 +584,7 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { VpcPrefixID: &vpcPrefixID, VpcPrefix: vpcPrefix, // We attach this here so it can be used when we convert to the API model. RequestedIpAddress: ifc.IPAddress, + RoutingProfile: ifc.RoutingProfile.ToDBModel(), Device: ifc.Device, DeviceInstance: ifc.DeviceInstance, VirtualFunctionID: ifc.VirtualFunctionID, @@ -1335,6 +1336,7 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { DeviceInstance: dbifc.DeviceInstance, VirtualFunctionID: dbifc.VirtualFunctionID, RequestedIpAddress: dbifc.RequestedIpAddress, + RoutingProfile: dbifc.RoutingProfile, IsPhysical: dbifc.IsPhysical, Status: dbifc.Status, CreatedBy: dbUser.ID, @@ -1393,6 +1395,9 @@ func (cih CreateInstanceHandler) Handle(c echo.Context) error { if dbifc.RequestedIpAddress != nil { interfaceConfig.IpAddress = dbifc.RequestedIpAddress } + if dbifc.RoutingProfile != nil { + interfaceConfig.RoutingProfile = dbifc.RoutingProfile.ToProto() + } interfaceConfigs = append(interfaceConfigs, interfaceConfig) } @@ -2435,6 +2440,7 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { VpcPrefixID: &vpcPrefixID, VpcPrefix: vpcPrefix, // We attach this here so it can be used when we convert to the API model. RequestedIpAddress: ifc.IPAddress, + RoutingProfile: ifc.RoutingProfile.ToDBModel(), Device: ifc.Device, DeviceInstance: ifc.DeviceInstance, VirtualFunctionID: ifc.VirtualFunctionID, @@ -3013,6 +3019,7 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { DeviceInstance: dbifc.DeviceInstance, VirtualFunctionID: dbifc.VirtualFunctionID, RequestedIpAddress: dbifc.RequestedIpAddress, + RoutingProfile: dbifc.RoutingProfile, IsPhysical: dbifc.IsPhysical, Status: dbifc.Status, CreatedBy: dbUser.ID, @@ -3434,6 +3441,9 @@ func (uih UpdateInstanceHandler) Handle(c echo.Context) error { if ifc.RequestedIpAddress != nil { interfaceConfig.IpAddress = ifc.RequestedIpAddress } + if ifc.RoutingProfile != nil { + interfaceConfig.RoutingProfile = ifc.RoutingProfile.ToProto() + } interfaceConfigs[i] = interfaceConfig } diff --git a/api/pkg/api/handler/instance_test.go b/api/pkg/api/handler/instance_test.go index ac0e48110..cabfa2c1c 100644 --- a/api/pkg/api/handler/instance_test.go +++ b/api/pkg/api/handler/instance_test.go @@ -626,6 +626,16 @@ func testUpdateOSIsActive(t *testing.T, dbSession *cdb.Session, ins *cdbm.Operat return ins } +// assertInterfaceRoutingProfilePrefixes verifies proto routing profile prefix order. +func assertInterfaceRoutingProfilePrefixes(t *testing.T, actual *cwssaws.InstanceInterfaceRoutingProfile, expected []string) { + t.Helper() + require.NotNil(t, actual) + require.Len(t, actual.AllowedAnycastPrefixes, len(expected)) + for i, prefix := range expected { + assert.Equal(t, prefix, actual.AllowedAnycastPrefixes[i].Prefix) + } +} + func TestCreateInstanceHandler_Handle(t *testing.T) { ctx := context.Background() @@ -1646,6 +1656,9 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { IsPhysical: true, Device: cdb.GetStrPtr("MT42822 BlueField-2 integrated ConnectX-6 Dx network controller"), DeviceInstance: cdb.GetIntPtr(0), + RoutingProfile: &model.APIInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, }, { VpcPrefixID: cdb.GetStrPtr(vpcPrefix5.ID.String()), @@ -3464,6 +3477,7 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { assert.Equal(t, len(rst.StatusHistory), 1) if len(tt.args.reqData.Interfaces) > 0 { require.Len(t, rst.Interfaces, len(tt.args.reqData.Interfaces)) + hasRoutingProfile := false for i := range tt.args.reqData.Interfaces { if tt.args.reqData.Interfaces[i].SubnetID != nil { assert.Equal(t, tt.args.reqData.Interfaces[i].SubnetID, rst.Interfaces[i].SubnetID) @@ -3480,6 +3494,11 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { assert.Equal(t, tt.args.reqData.Interfaces[i].Device, rst.Interfaces[i].Device) assert.Equal(t, tt.args.reqData.Interfaces[i].DeviceInstance, rst.Interfaces[i].DeviceInstance) assert.Equal(t, tt.args.reqData.Interfaces[i].VirtualFunctionID, rst.Interfaces[i].VirtualFunctionID) + if tt.args.reqData.Interfaces[i].RoutingProfile != nil { + hasRoutingProfile = true + require.NotNil(t, rst.Interfaces[i].RoutingProfile) + assert.Equal(t, tt.args.reqData.Interfaces[i].RoutingProfile.AllowedAnycastPrefixes, rst.Interfaces[i].RoutingProfile.AllowedAnycastPrefixes) + } // Handle the fact that single-interface instance get normalized to have // PF for the first interface. @@ -3487,6 +3506,22 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { assert.Equal(t, tt.args.reqData.Interfaces[i].IsPhysical, rst.Interfaces[i].IsPhysical) } } + + if hasRoutingProfile { + ifcDAO := cdbm.NewInterfaceDAO(dbSession) + dbIfcs, _, ierr := ifcDAO.GetAll(ec.Request().Context(), nil, + cdbm.InterfaceFilterInput{InstanceIDs: []uuid.UUID{uuid.MustParse(rst.ID)}}, + cdbp.PageInput{OrderBy: &cdbp.OrderBy{Field: cdbm.InterfaceOrderByCreated, Order: cdbp.OrderAscending}}, + nil) + require.NoError(t, ierr) + require.Len(t, dbIfcs, len(tt.args.reqData.Interfaces)) + for i := range tt.args.reqData.Interfaces { + if tt.args.reqData.Interfaces[i].RoutingProfile != nil { + require.NotNil(t, dbIfcs[i].RoutingProfile) + assert.Equal(t, tt.args.reqData.Interfaces[i].RoutingProfile.AllowedAnycastPrefixes, dbIfcs[i].RoutingProfile.AllowedAnycastPrefixes) + } + } + } } if len(tsc.Calls) > 0 && len(tsc.Calls[len(tsc.Calls)-1].Arguments) > 3 { @@ -3512,6 +3547,12 @@ func TestCreateInstanceHandler_Handle(t *testing.T) { } else { assert.False(t, req.AllowUnhealthyMachine, fmt.Sprintf("%v", req)) } + + for i, reqIfc := range tt.args.reqData.Interfaces { + if reqIfc.RoutingProfile != nil { + assertInterfaceRoutingProfilePrefixes(t, req.Config.Network.Interfaces[i].RoutingProfile, reqIfc.RoutingProfile.AllowedAnycastPrefixes) + } + } } if len(tt.args.reqData.InfiniBandInterfaces) > 0 { @@ -5613,6 +5654,9 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { IsPhysical: true, Device: cdb.GetStrPtr("MT42822 BlueField-2 integrated ConnectX-6 Dx network controller"), DeviceInstance: cdb.GetIntPtr(0), + RoutingProfile: &model.APIInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, }, }, }, @@ -6618,6 +6662,11 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { assert.Equal(t, tt.args.reqData.Interfaces[i].IPAddress, ifcs[i].RequestedIpAddress) } + if tt.args.reqData.Interfaces[i].RoutingProfile != nil { + require.NotNil(t, ifcs[i].RoutingProfile) + assert.Equal(t, tt.args.reqData.Interfaces[i].RoutingProfile.AllowedAnycastPrefixes, ifcs[i].RoutingProfile.AllowedAnycastPrefixes) + } + assert.Equal(t, cdbm.InterfaceStatusPending, ifcs[i].Status) } } @@ -6790,6 +6839,10 @@ func TestUpdateInstanceHandler_Handle(t *testing.T) { if reqInsIfcs[i].RequestedIpAddress != nil { assert.Equal(t, siteIfc.IpAddress, reqInsIfcs[i].RequestedIpAddress) } + + if tt.args.reqData.Interfaces != nil && i < len(tt.args.reqData.Interfaces) && tt.args.reqData.Interfaces[i].RoutingProfile != nil { + assertInterfaceRoutingProfilePrefixes(t, siteIfc.RoutingProfile, tt.args.reqData.Interfaces[i].RoutingProfile.AllowedAnycastPrefixes) + } } // Verify the InfiniBand Interfaces are in the Site Controller request diff --git a/api/pkg/api/handler/instancebatch.go b/api/pkg/api/handler/instancebatch.go index cea1643d9..97ef6313d 100644 --- a/api/pkg/api/handler/instancebatch.go +++ b/api/pkg/api/handler/instancebatch.go @@ -627,6 +627,7 @@ func (bcih BatchCreateInstanceHandler) Handle(c echo.Context) error { VpcPrefixID: &vpcPrefixUUID, VpcPrefix: vpcPrefix, RequestedIpAddress: nil, // Explicit IPs are not supported for batch create. + RoutingProfile: ifc.RoutingProfile.ToDBModel(), Device: ifc.Device, DeviceInstance: ifc.DeviceInstance, VirtualFunctionID: ifc.VirtualFunctionID, @@ -1336,6 +1337,7 @@ func (bcih BatchCreateInstanceHandler) Handle(c echo.Context) error { DeviceInstance: dbifc.DeviceInstance, VirtualFunctionID: dbifc.VirtualFunctionID, RequestedIpAddress: dbifc.RequestedIpAddress, + RoutingProfile: dbifc.RoutingProfile, IsPhysical: dbifc.IsPhysical, Status: cdbm.InterfaceStatusPending, CreatedBy: dbUser.ID, @@ -1535,6 +1537,9 @@ func (bcih BatchCreateInstanceHandler) Handle(c echo.Context) error { vfID := uint32(*ifc.VirtualFunctionID) interfaceConfig.VirtualFunctionId = &vfID } + if ifc.RoutingProfile != nil { + interfaceConfig.RoutingProfile = ifc.RoutingProfile.ToProto() + } createdInstancesData[idx].interfaceConfigs = append(createdInstancesData[idx].interfaceConfigs, interfaceConfig) } diff --git a/api/pkg/api/handler/instancebatch_test.go b/api/pkg/api/handler/instancebatch_test.go index 4bbbdc527..d425a2b96 100644 --- a/api/pkg/api/handler/instancebatch_test.go +++ b/api/pkg/api/handler/instancebatch_test.go @@ -19,12 +19,14 @@ import ( authz "github.com/NVIDIA/infra-controller-rest/auth/pkg/authorization" cdb "github.com/NVIDIA/infra-controller-rest/db/pkg/db" cdbm "github.com/NVIDIA/infra-controller-rest/db/pkg/db/model" + cdbp "github.com/NVIDIA/infra-controller-rest/db/pkg/db/paginator" cdbu "github.com/NVIDIA/infra-controller-rest/db/pkg/util" cwssaws "github.com/NVIDIA/infra-controller-rest/workflow-schema/schema/site-agent/workflows/v1" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/uptrace/bun/extra/bundebug" tmocks "go.temporal.io/sdk/mocks" ) @@ -447,6 +449,9 @@ func TestBatchCreateInstanceHandler_Handle(t *testing.T) { IsPhysical: true, Device: cdb.GetStrPtr("MT42822 BlueField-2 integrated ConnectX-6 Dx network controller"), DeviceInstance: cdb.GetIntPtr(0), + RoutingProfile: &model.APIInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, }, { VpcPrefixID: cdb.GetStrPtr(vpcPrefixSecondary.ID.String()), @@ -1302,6 +1307,14 @@ func TestBatchCreateInstanceHandler_Handle(t *testing.T) { assert.Nil(t, jsonErr) assert.Equal(t, tt.args.reqData.Count, len(response), "Expected %d instances, got %d", tt.args.reqData.Count, len(response)) + hasRoutingProfile := false + for _, reqIfc := range tt.args.reqData.Interfaces { + if reqIfc.RoutingProfile != nil { + hasRoutingProfile = true + break + } + } + // Verify instance names follow the pattern: namePrefix-randomSuffix-index for i, inst := range response { expectedPrefix := tt.args.reqData.NamePrefix + "-" @@ -1313,6 +1326,51 @@ func TestBatchCreateInstanceHandler_Handle(t *testing.T) { } else { assert.Empty(t, inst.SecondaryVpcIDs) } + + if hasRoutingProfile { + require.Len(t, inst.Interfaces, len(tt.args.reqData.Interfaces)) + for j, reqIfc := range tt.args.reqData.Interfaces { + if reqIfc.RoutingProfile != nil { + require.NotNil(t, inst.Interfaces[j].RoutingProfile) + assert.Equal(t, reqIfc.RoutingProfile.AllowedAnycastPrefixes, inst.Interfaces[j].RoutingProfile.AllowedAnycastPrefixes) + } + } + + ifcDAO := cdbm.NewInterfaceDAO(dbSession) + dbIfcs, _, ierr := ifcDAO.GetAll(ec.Request().Context(), nil, + cdbm.InterfaceFilterInput{InstanceIDs: []uuid.UUID{uuid.MustParse(inst.ID)}}, + cdbp.PageInput{OrderBy: &cdbp.OrderBy{Field: cdbm.InterfaceOrderByCreated, Order: cdbp.OrderAscending}}, + nil) + require.NoError(t, ierr) + require.Len(t, dbIfcs, len(tt.args.reqData.Interfaces)) + for j, reqIfc := range tt.args.reqData.Interfaces { + if reqIfc.RoutingProfile != nil { + require.NotNil(t, dbIfcs[j].RoutingProfile) + assert.Equal(t, reqIfc.RoutingProfile.AllowedAnycastPrefixes, dbIfcs[j].RoutingProfile.AllowedAnycastPrefixes) + } + } + } + } + + if hasRoutingProfile { + var batchReq *cwssaws.BatchInstanceAllocationRequest + for i := len(tsc.Calls) - 1; i >= 0; i-- { + call := tsc.Calls[i] + if call.Method == "ExecuteWorkflow" && len(call.Arguments) > 3 && call.Arguments[2] == "CreateInstances" { + batchReq = call.Arguments[3].(*cwssaws.BatchInstanceAllocationRequest) + break + } + } + require.NotNil(t, batchReq) + require.Len(t, batchReq.InstanceRequests, len(response)) + for _, instReq := range batchReq.InstanceRequests { + require.Len(t, instReq.Config.Network.Interfaces, len(tt.args.reqData.Interfaces)) + for j, reqIfc := range tt.args.reqData.Interfaces { + if reqIfc.RoutingProfile != nil { + assertInterfaceRoutingProfilePrefixes(t, instReq.Config.Network.Interfaces[j].RoutingProfile, reqIfc.RoutingProfile.AllowedAnycastPrefixes) + } + } + } } } } diff --git a/api/pkg/api/model/interface.go b/api/pkg/api/model/interface.go index 8a8abf713..3c36eedc8 100644 --- a/api/pkg/api/model/interface.go +++ b/api/pkg/api/model/interface.go @@ -5,6 +5,7 @@ package model import ( "errors" + "fmt" "net/netip" "time" @@ -23,6 +24,8 @@ type APIInterfaceCreateOrUpdateRequest struct { VpcPrefixID *string `json:"vpcPrefixId"` // IPAddress is the explicitly requested IP address for the Interface IPAddress *string `json:"ipAddress"` + // RoutingProfile narrows VPC routing-profile options for this Interface + RoutingProfile *APIInterfaceRoutingProfile `json:"routingProfile"` // Device is the device name of the Interface Device *string `json:"device"` // DeviceInstance is the ID of the DeviceInstance @@ -33,6 +36,66 @@ type APIInterfaceCreateOrUpdateRequest struct { IsPhysical bool `json:"isPhysical"` } +// APIInterfaceRoutingProfile captures interface-local routing profile options. +type APIInterfaceRoutingProfile struct { + // AllowedAnycastPrefixes is the list of CIDR prefixes this interface may announce as anycast. + AllowedAnycastPrefixes []string `json:"allowedAnycastPrefixes"` +} + +// Validate ensures the routing profile contains valid CIDR prefixes. +func (rp *APIInterfaceRoutingProfile) Validate() error { + if rp == nil { + return nil + } + if rp.AllowedAnycastPrefixes == nil { + rp.AllowedAnycastPrefixes = []string{} + } + err := validation.Validate(rp.AllowedAnycastPrefixes, + validation.Each(validation.By(validateInterfaceAnycastPrefix)), + ) + if err != nil { + return validation.Errors{ + "allowedAnycastPrefixes": err, + } + } + return nil +} + +func validateInterfaceAnycastPrefix(value any) error { + prefix, ok := value.(string) + if !ok { + return nil + } + if _, err := netip.ParsePrefix(prefix); err != nil { + return fmt.Errorf("invalid anycast prefix `%s`", prefix) + } + return nil +} + +// ToDBModel converts the API routing profile into the DB model shape. +func (rp *APIInterfaceRoutingProfile) ToDBModel() *cdbm.InterfaceRoutingProfile { + if rp == nil { + return nil + } + prefixes := []string{} + if rp.AllowedAnycastPrefixes != nil { + prefixes = append(prefixes, rp.AllowedAnycastPrefixes...) + } + return &cdbm.InterfaceRoutingProfile{AllowedAnycastPrefixes: prefixes} +} + +// NewAPIInterfaceRoutingProfile converts a DB routing profile into its API response shape. +func NewAPIInterfaceRoutingProfile(dbProfile *cdbm.InterfaceRoutingProfile) *APIInterfaceRoutingProfile { + if dbProfile == nil { + return nil + } + prefixes := []string{} + if dbProfile.AllowedAnycastPrefixes != nil { + prefixes = append(prefixes, dbProfile.AllowedAnycastPrefixes...) + } + return &APIInterfaceRoutingProfile{AllowedAnycastPrefixes: prefixes} +} + func (ifcr APIInterfaceCreateOrUpdateRequest) IsMultiEthernetInterface() bool { return ifcr.Device != nil && ifcr.DeviceInstance != nil } @@ -66,6 +129,7 @@ func (ifcr APIInterfaceCreateOrUpdateRequest) Validate() error { validation.Field(&ifcr.IPAddress, validation.When(ifcr.IPAddress != nil, validation.By(validateInterfaceRequestedIpAddressHostBit)), ), + validation.Field(&ifcr.RoutingProfile), validation.Field(&ifcr.DeviceInstance, validation.Min(0).Error("deviceInstance must be equal or greater than 0")), validation.Field(&ifcr.VirtualFunctionID, @@ -85,6 +149,12 @@ func (ifcr APIInterfaceCreateOrUpdateRequest) Validate() error { } } + if ifcr.RoutingProfile != nil && ifcr.SubnetID != nil { + return validation.Errors{ + "routingProfile": errors.New("cannot be specified for Subnet based Interfaces"), + } + } + if ifcr.SubnetID == nil && ifcr.VpcPrefixID == nil { return validation.Errors{ validationCommonErrorField: errors.New("either `subnetId` or `vpcPrefixId` must be specified"), @@ -149,6 +219,8 @@ type APIInterface struct { IPAddresses []string `json:"ipAddresses"` // RequestedIpAddress is the explicitly requested IP address for the Interface RequestedIpAddress *string `json:"requestedIpAddress"` + // RoutingProfile contains interface-local routing profile options. + RoutingProfile *APIInterfaceRoutingProfile `json:"routingProfile"` // Status is the status of the Interface Status string `json:"status"` // Created is the date and time the entity was created @@ -166,6 +238,7 @@ func NewAPIInterface(dbis *cdbm.Interface) *APIInterface { MacAddress: dbis.MacAddress, IPAddresses: dbis.IPAddresses, RequestedIpAddress: dbis.RequestedIpAddress, + RoutingProfile: NewAPIInterfaceRoutingProfile(dbis.RoutingProfile), Status: dbis.Status, Created: dbis.Created, Updated: dbis.Updated, diff --git a/api/pkg/api/model/interface_test.go b/api/pkg/api/model/interface_test.go index 292cd8506..8343cb0b7 100644 --- a/api/pkg/api/model/interface_test.go +++ b/api/pkg/api/model/interface_test.go @@ -12,6 +12,7 @@ import ( cdbm "github.com/NVIDIA/infra-controller-rest/db/pkg/db/model" "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewAPIInterface(t *testing.T) { @@ -60,6 +61,124 @@ func TestNewAPIInterface(t *testing.T) { } } +func TestAPIInterfaceCreateOrUpdateRequest_RoutingProfileValidate(t *testing.T) { + tests := []struct { + name string + req APIInterfaceCreateOrUpdateRequest + wantErr bool + wantErrorContains []string + wantAllowedAnycastPrefixes []string + wantNonNilPrefixes bool + }{ + { + name: "VPC Prefix interface accepts IPv4 and IPv6 anycast prefixes", + req: APIInterfaceCreateOrUpdateRequest{ + VpcPrefixID: cdb.GetStrPtr(uuid.NewString()), + RoutingProfile: &APIInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, + }, + wantAllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, + { + name: "explicit empty routing profile stays non-nil with empty anycast prefixes", + req: APIInterfaceCreateOrUpdateRequest{ + VpcPrefixID: cdb.GetStrPtr(uuid.NewString()), + RoutingProfile: &APIInterfaceRoutingProfile{}, + }, + wantAllowedAnycastPrefixes: []string{}, + wantNonNilPrefixes: true, + }, + { + name: "invalid anycast prefix returns field-specific error", + req: APIInterfaceCreateOrUpdateRequest{ + VpcPrefixID: cdb.GetStrPtr(uuid.NewString()), + RoutingProfile: &APIInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"not-a-prefix"}, + }, + }, + wantErr: true, + wantErrorContains: []string{"allowedAnycastPrefixes", "not-a-prefix"}, + }, + { + name: "Subnet interface rejects routing profile", + req: APIInterfaceCreateOrUpdateRequest{ + SubnetID: cdb.GetStrPtr(uuid.NewString()), + RoutingProfile: &APIInterfaceRoutingProfile{}, + }, + wantErr: true, + wantErrorContains: []string{"routingProfile", "cannot be specified for Subnet based Interfaces"}, + }, + { + name: "Subnet interface accepts nil routing profile", + req: APIInterfaceCreateOrUpdateRequest{ + SubnetID: cdb.GetStrPtr(uuid.NewString()), + }, + }, + { + name: "VPC Prefix interface accepts nil routing profile", + req: APIInterfaceCreateOrUpdateRequest{ + VpcPrefixID: cdb.GetStrPtr(uuid.NewString()), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + if tt.wantErr { + require.Error(t, err) + for _, want := range tt.wantErrorContains { + assert.Contains(t, err.Error(), want) + } + return + } + + assert.NoError(t, err) + if tt.req.RoutingProfile != nil { + if tt.wantNonNilPrefixes { + assert.NotNil(t, tt.req.RoutingProfile.AllowedAnycastPrefixes) + } + assert.Equal(t, tt.wantAllowedAnycastPrefixes, tt.req.RoutingProfile.AllowedAnycastPrefixes) + } + }) + } +} + +func TestAPIInterfaceRoutingProfile_ToDBModel(t *testing.T) { + var nilProfile *APIInterfaceRoutingProfile + assert.Nil(t, nilProfile.ToDBModel()) + + emptyProfile := (&APIInterfaceRoutingProfile{}).ToDBModel() + require.NotNil(t, emptyProfile) + assert.NotNil(t, emptyProfile.AllowedAnycastPrefixes) + assert.Empty(t, emptyProfile.AllowedAnycastPrefixes) + + apiProfile := &APIInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + } + dbProfile := apiProfile.ToDBModel() + require.NotNil(t, dbProfile) + assert.Equal(t, []string{"192.0.2.0/24", "2001:db8::/64"}, dbProfile.AllowedAnycastPrefixes) + + apiProfile.AllowedAnycastPrefixes[0] = "198.51.100.0/24" + assert.Equal(t, []string{"192.0.2.0/24", "2001:db8::/64"}, dbProfile.AllowedAnycastPrefixes) +} + +func TestNewAPIInterfaceRoutingProfile(t *testing.T) { + assert.Nil(t, NewAPIInterfaceRoutingProfile(nil)) + + dbProfile := &cdbm.InterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + } + apiProfile := NewAPIInterfaceRoutingProfile(dbProfile) + require.NotNil(t, apiProfile) + assert.Equal(t, []string{"192.0.2.0/24", "2001:db8::/64"}, apiProfile.AllowedAnycastPrefixes) + + dbProfile.AllowedAnycastPrefixes[0] = "198.51.100.0/24" + assert.Equal(t, []string{"192.0.2.0/24", "2001:db8::/64"}, apiProfile.AllowedAnycastPrefixes) +} + func TestAPIInterfaceCreateRequest_Validate(t *testing.T) { type fields struct { SubnetID *string diff --git a/common/pkg/util/converter_test.go b/common/pkg/util/converter_test.go index f7c05b5d4..c5dd1ef78 100644 --- a/common/pkg/util/converter_test.go +++ b/common/pkg/util/converter_test.go @@ -1,19 +1,5 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 package util diff --git a/db/pkg/db/model/interface.go b/db/pkg/db/model/interface.go index 0fd964510..c7126aa89 100644 --- a/db/pkg/db/model/interface.go +++ b/db/pkg/db/model/interface.go @@ -11,9 +11,9 @@ import ( "github.com/NVIDIA/infra-controller-rest/db/pkg/db" "github.com/NVIDIA/infra-controller-rest/db/pkg/db/paginator" - "github.com/google/uuid" - stracer "github.com/NVIDIA/infra-controller-rest/db/pkg/tracer" + cwssaws "github.com/NVIDIA/infra-controller-rest/workflow-schema/schema/site-agent/workflows/v1" + "github.com/google/uuid" "github.com/uptrace/bun" ) @@ -57,31 +57,73 @@ var ( } ) +// InterfaceRoutingProfile is the DB representation of interface-local routing options. +type InterfaceRoutingProfile struct { + AllowedAnycastPrefixes []string `json:"allowedAnycastPrefixes"` +} + +// ToProto converts this interface routing profile into its workflow proto representation. +func (irp *InterfaceRoutingProfile) ToProto() *cwssaws.InstanceInterfaceRoutingProfile { + if irp == nil { + return nil + } + profile := &cwssaws.InstanceInterfaceRoutingProfile{ + AllowedAnycastPrefixes: make([]*cwssaws.PrefixFilterPolicyEntry, 0, len(irp.AllowedAnycastPrefixes)), + } + for _, prefix := range irp.AllowedAnycastPrefixes { + profile.AllowedAnycastPrefixes = append(profile.AllowedAnycastPrefixes, &cwssaws.PrefixFilterPolicyEntry{Prefix: prefix}) + } + return profile +} + +// FromProto populates this routing profile from its workflow proto representation. +func (irp *InterfaceRoutingProfile) FromProto(proto *cwssaws.InstanceInterfaceRoutingProfile) { + if proto == nil { + *irp = InterfaceRoutingProfile{} + return + } + irp.AllowedAnycastPrefixes = make([]string, 0, len(proto.GetAllowedAnycastPrefixes())) + for _, entry := range proto.GetAllowedAnycastPrefixes() { + irp.AllowedAnycastPrefixes = append(irp.AllowedAnycastPrefixes, entry.GetPrefix()) + } +} + +// NewInterfaceRoutingProfile builds a DB routing profile from a workflow proto. +func NewInterfaceRoutingProfile(proto *cwssaws.InstanceInterfaceRoutingProfile) *InterfaceRoutingProfile { + if proto == nil { + return nil + } + profile := &InterfaceRoutingProfile{} + profile.FromProto(proto) + return profile +} + // Interface table maintains association between an instance and a subnet type Interface struct { bun.BaseModel `bun:"table:interface,alias:ifc"` - ID uuid.UUID `bun:"type:uuid,pk"` - InstanceID uuid.UUID `bun:"instance_id,type:uuid,notnull"` - Instance *Instance `bun:"rel:belongs-to,join:instance_id=id"` - SubnetID *uuid.UUID `bun:"subnet_id,type:uuid"` - Subnet *Subnet `bun:"rel:belongs-to,join:subnet_id=id"` - VpcPrefixID *uuid.UUID `bun:"vpc_prefix_id,type:uuid"` - VpcPrefix *VpcPrefix `bun:"rel:belongs-to,join:vpc_prefix_id=id"` - MachineInterfaceID *uuid.UUID `bun:"machine_interface_id,type:uuid"` - MachineInterface *MachineInterface `bun:"rel:belongs-to,join:machine_interface_id=id"` - Device *string `bun:"device"` - DeviceInstance *int `bun:"device_instance"` - IsPhysical bool `bun:"is_physical,notnull"` - VirtualFunctionID *int `bun:"virtual_function_id"` - RequestedIpAddress *string `bun:"requested_ip_address"` - MacAddress *string `bun:"mac_address"` - IPAddresses []string `bun:"ip_addresses,type:text[]"` - Status string `bun:"status,notnull"` - Created time.Time `bun:"created,nullzero,notnull,default:current_timestamp"` - Updated time.Time `bun:"updated,nullzero,notnull,default:current_timestamp"` - Deleted *time.Time `bun:"deleted,soft_delete"` - CreatedBy uuid.UUID `bun:"type:uuid,notnull"` + ID uuid.UUID `bun:"type:uuid,pk"` + InstanceID uuid.UUID `bun:"instance_id,type:uuid,notnull"` + Instance *Instance `bun:"rel:belongs-to,join:instance_id=id"` + SubnetID *uuid.UUID `bun:"subnet_id,type:uuid"` + Subnet *Subnet `bun:"rel:belongs-to,join:subnet_id=id"` + VpcPrefixID *uuid.UUID `bun:"vpc_prefix_id,type:uuid"` + VpcPrefix *VpcPrefix `bun:"rel:belongs-to,join:vpc_prefix_id=id"` + MachineInterfaceID *uuid.UUID `bun:"machine_interface_id,type:uuid"` + MachineInterface *MachineInterface `bun:"rel:belongs-to,join:machine_interface_id=id"` + Device *string `bun:"device"` + DeviceInstance *int `bun:"device_instance"` + IsPhysical bool `bun:"is_physical,notnull"` + VirtualFunctionID *int `bun:"virtual_function_id"` + RequestedIpAddress *string `bun:"requested_ip_address"` + MacAddress *string `bun:"mac_address"` + IPAddresses []string `bun:"ip_addresses,type:text[]"` + RoutingProfile *InterfaceRoutingProfile `bun:"routing_profile,type:jsonb"` + Status string `bun:"status,notnull"` + Created time.Time `bun:"created,nullzero,notnull,default:current_timestamp"` + Updated time.Time `bun:"updated,nullzero,notnull,default:current_timestamp"` + Deleted *time.Time `bun:"deleted,soft_delete"` + CreatedBy uuid.UUID `bun:"type:uuid,notnull"` } // InterfaceCreateInput input parameters for Create method @@ -94,6 +136,7 @@ type InterfaceCreateInput struct { DeviceInstance *int VirtualFunctionID *int RequestedIpAddress *string + RoutingProfile *InterfaceRoutingProfile Status string CreatedBy uuid.UUID } @@ -108,6 +151,7 @@ type InterfaceUpdateInput struct { DeviceInstance *int VirtualFunctionID *int RequestedIpAddress *string + RoutingProfile *InterfaceRoutingProfile MacAddress *string IpAddresses []string Status *string @@ -129,6 +173,7 @@ type InterfaceFilterInput struct { type InterfaceClearInput struct { InterfaceID uuid.UUID RequestedIpAddress bool + RoutingProfile bool } var _ bun.BeforeAppendModelHook = (*Interface)(nil) @@ -418,6 +463,10 @@ func (ifcd InterfaceSQLDAO) Update(ctx context.Context, tx *db.Tx, input Interfa ifcd.tracerSpan.SetAttribute(interfaceDAOSpan, "requested_ip_address", *input.RequestedIpAddress) } } + if input.RoutingProfile != nil { + is.RoutingProfile = input.RoutingProfile + updatedFields = append(updatedFields, "routing_profile") + } if input.MacAddress != nil { is.MacAddress = input.MacAddress updatedFields = append(updatedFields, "mac_address") @@ -548,6 +597,7 @@ func (ifcd InterfaceSQLDAO) CreateMultiple(ctx context.Context, tx *db.Tx, input DeviceInstance: input.DeviceInstance, VirtualFunctionID: input.VirtualFunctionID, RequestedIpAddress: input.RequestedIpAddress, + RoutingProfile: input.RoutingProfile, IsPhysical: input.IsPhysical, Status: input.Status, CreatedBy: input.CreatedBy, @@ -614,6 +664,10 @@ func (ifcd InterfaceSQLDAO) Clear(ctx context.Context, tx *db.Tx, input Interfac i.RequestedIpAddress = nil updatedFields = append(updatedFields, "requested_ip_address") } + if input.RoutingProfile { + i.RoutingProfile = nil + updatedFields = append(updatedFields, "routing_profile") + } if len(updatedFields) > 0 { updatedFields = append(updatedFields, "updated") diff --git a/db/pkg/db/model/interface_test.go b/db/pkg/db/model/interface_test.go index 71219e668..dbd2916a7 100644 --- a/db/pkg/db/model/interface_test.go +++ b/db/pkg/db/model/interface_test.go @@ -10,6 +10,7 @@ import ( "github.com/NVIDIA/infra-controller-rest/db/pkg/db" "github.com/NVIDIA/infra-controller-rest/db/pkg/db/paginator" stracer "github.com/NVIDIA/infra-controller-rest/db/pkg/tracer" + cwssaws "github.com/NVIDIA/infra-controller-rest/workflow-schema/schema/site-agent/workflows/v1" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,6 +75,43 @@ func testInterfaceSetupSchema(t *testing.T, dbSession *db.Session) { assert.Nil(t, err) } +func TestInterfaceRoutingProfile_ToProtoFromProto(t *testing.T) { + var nilProfile *InterfaceRoutingProfile + assert.Nil(t, nilProfile.ToProto()) + assert.Nil(t, NewInterfaceRoutingProfile(nil)) + + emptyProfile := &InterfaceRoutingProfile{} + emptyProto := emptyProfile.ToProto() + require.NotNil(t, emptyProto) + assert.NotNil(t, emptyProto.AllowedAnycastPrefixes) + assert.Empty(t, emptyProto.AllowedAnycastPrefixes) + + profile := &InterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + } + protoProfile := profile.ToProto() + require.NotNil(t, protoProfile) + require.Len(t, protoProfile.AllowedAnycastPrefixes, 2) + assert.Equal(t, "192.0.2.0/24", protoProfile.AllowedAnycastPrefixes[0].Prefix) + assert.Equal(t, "2001:db8::/64", protoProfile.AllowedAnycastPrefixes[1].Prefix) + + var roundTrip InterfaceRoutingProfile + roundTrip.FromProto(protoProfile) + assert.Equal(t, profile.AllowedAnycastPrefixes, roundTrip.AllowedAnycastPrefixes) + + roundTrip.FromProto(nil) + assert.Nil(t, roundTrip.AllowedAnycastPrefixes) + + fromProto := NewInterfaceRoutingProfile(&cwssaws.InstanceInterfaceRoutingProfile{ + AllowedAnycastPrefixes: []*cwssaws.PrefixFilterPolicyEntry{ + {Prefix: "198.51.100.0/24"}, + {Prefix: "2001:db8:1::/64"}, + }, + }) + require.NotNil(t, fromProto) + assert.Equal(t, []string{"198.51.100.0/24", "2001:db8:1::/64"}, fromProto.AllowedAnycastPrefixes) +} + func TestInterfaceSQLDAO_Create(t *testing.T) { ctx := context.Background() dbSession := testInstanceInitDB(t) @@ -196,9 +234,12 @@ func TestInterfaceSQLDAO_Create(t *testing.T) { InstanceID: i1.ID, VpcPrefixID: &vpcPrefix.ID, RequestedIpAddress: db.GetStrPtr("192.0.2.11"), - IsPhysical: false, - Status: InterfaceStatusPending, - CreatedBy: user.ID, + RoutingProfile: &InterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, + IsPhysical: false, + Status: InterfaceStatusPending, + CreatedBy: user.ID, }, }, expectError: false, @@ -279,6 +320,7 @@ func TestInterfaceSQLDAO_Create(t *testing.T) { IsPhysical: i.IsPhysical, VirtualFunctionID: i.VirtualFunctionID, RequestedIpAddress: i.RequestedIpAddress, + RoutingProfile: i.RoutingProfile, Status: i.Status, CreatedBy: i.CreatedBy, } @@ -304,6 +346,8 @@ func TestInterfaceSQLDAO_Create(t *testing.T) { } assert.Equal(t, i.RequestedIpAddress, got.RequestedIpAddress) assert.Equal(t, i.RequestedIpAddress, persisted.RequestedIpAddress) + assert.Equal(t, i.RoutingProfile, got.RoutingProfile) + assert.Equal(t, i.RoutingProfile, persisted.RoutingProfile) } } @@ -987,11 +1031,15 @@ func TestInterfaceSQLDAO_Clear(t *testing.T) { ifcd := NewInterfaceDAO(dbSession) requestedIpAddress := db.GetStrPtr("192.0.2.11") + routingProfile := &InterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + } ifc, err := ifcd.Create(ctx, nil, InterfaceCreateInput{ InstanceID: instance.ID, SubnetID: &subnet.ID, IsPhysical: true, RequestedIpAddress: requestedIpAddress, + RoutingProfile: routingProfile, Status: InterfaceStatusPending, CreatedBy: user.ID, }) @@ -1010,6 +1058,7 @@ func TestInterfaceSQLDAO_Clear(t *testing.T) { input InterfaceClearInput expectError bool expectRequestedIP *string + expectRouting *InterfaceRoutingProfile verifyChildSpanner bool }{ { @@ -1018,6 +1067,16 @@ func TestInterfaceSQLDAO_Clear(t *testing.T) { input: InterfaceClearInput{InterfaceID: ifc.ID, RequestedIpAddress: true}, expectError: true, expectRequestedIP: requestedIpAddress, + expectRouting: routingProfile, + verifyChildSpanner: true, + }, + { + desc: "can clear routing profile", + dao: ifcd, + input: InterfaceClearInput{InterfaceID: ifc.ID, RoutingProfile: true}, + expectError: false, + expectRequestedIP: requestedIpAddress, + expectRouting: nil, verifyChildSpanner: true, }, { @@ -1026,6 +1085,7 @@ func TestInterfaceSQLDAO_Clear(t *testing.T) { input: InterfaceClearInput{InterfaceID: ifc.ID, RequestedIpAddress: true}, expectError: false, expectRequestedIP: nil, + expectRouting: nil, verifyChildSpanner: true, }, } @@ -1040,6 +1100,7 @@ func TestInterfaceSQLDAO_Clear(t *testing.T) { } assert.Equal(t, tc.expectRequestedIP, got.RequestedIpAddress) + assert.Equal(t, tc.expectRouting, got.RoutingProfile) if tc.verifyChildSpanner { span := otrace.SpanFromContext(ctx) @@ -1216,6 +1277,9 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { vfID := 10 macAddress := "21-41-A7-A6-40-76" ipAddresses := []string{"192.0.2.3", "2001:db8:abcd:0018"} + routingProfile := &InterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + } input2 := InterfaceCreateInput{ InstanceID: i1.ID, @@ -1229,6 +1293,10 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, ifc1) + ifcRouting, err := ifcd.Create(ctx, nil, input2) + assert.Nil(t, err) + assert.NotNil(t, ifcRouting) + // OTEL Spanner configuration _, _, ctx = testCommonTraceProviderSetup(t, ctx) @@ -1242,6 +1310,7 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { paramDeviceInstance *int paramVirtualFunctionID *int paramRequestedIpAddress *string + paramRoutingProfile *InterfaceRoutingProfile paramMacAddress *string paramIPAddresses []string paramStatus *string @@ -1253,6 +1322,7 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { expectedDeviceInstance *int expectedVirtualFunctionID *int expectedRequestedIpAddress *string + expectedRoutingProfile *InterfaceRoutingProfile expectedMacAddress *string expectedIPAddresses []string expectedStatus *string @@ -1284,11 +1354,12 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { }, { desc: "success wth vpcprefix fields updated", - id: ifc1.ID, + id: ifcRouting.ID, paramInstanceID: &i2.ID, paramVpcPrefixID: &vpcPrefix2.ID, paramVirtualFunctionID: &vfID, paramRequestedIpAddress: db.GetStrPtr("192.0.2.31"), + paramRoutingProfile: routingProfile, paramMacAddress: &macAddress, paramIPAddresses: ipAddresses, paramStatus: db.GetStrPtr(InterfaceStatusReady), @@ -1297,6 +1368,7 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { expectedVpcPrefixID: &vpcPrefix2.ID, expectedVirtualFunctionID: &vfID, expectedRequestedIpAddress: db.GetStrPtr("192.0.2.31"), + expectedRoutingProfile: routingProfile, expectedMacAddress: &macAddress, expectedIPAddresses: ipAddresses, expectedStatus: db.GetStrPtr(InterfaceStatusReady), @@ -1346,6 +1418,7 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { DeviceInstance: tc.paramDeviceInstance, VirtualFunctionID: tc.paramVirtualFunctionID, RequestedIpAddress: tc.paramRequestedIpAddress, + RoutingProfile: tc.paramRoutingProfile, MacAddress: tc.paramMacAddress, IpAddresses: tc.paramIPAddresses, Status: tc.paramStatus, @@ -1365,10 +1438,15 @@ func TestInterfaceSQLDAO_Update(t *testing.T) { } assert.Equal(t, *tc.expectedVirtualFunctionID, *got.VirtualFunctionID) assert.Equal(t, tc.expectedRequestedIpAddress, got.RequestedIpAddress) + assert.Equal(t, tc.expectedRoutingProfile, got.RoutingProfile) assert.Equal(t, *tc.expectedMacAddress, *got.MacAddress) assert.Equal(t, tc.expectedIPAddresses, got.IPAddresses) assert.Equal(t, *tc.expectedStatus, got.Status) + persisted, err := ifcd.GetByID(ctx, nil, tc.id, nil) + require.NoError(t, err) + assert.Equal(t, tc.expectedRoutingProfile, persisted.RoutingProfile) + if tc.expectedDevice != nil { assert.Equal(t, *tc.expectedDevice, *got.Device) } @@ -1594,8 +1672,11 @@ func TestInterfaceSQLDAO_CreateMultiple(t *testing.T) { { InstanceID: instance1.ID, IsPhysical: true, - Status: InterfaceStatusPending, - CreatedBy: user.ID, + RoutingProfile: &InterfaceRoutingProfile{ + AllowedAnycastPrefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, + Status: InterfaceStatusPending, + CreatedBy: user.ID, }, { InstanceID: instance1.ID, @@ -1648,6 +1729,7 @@ func TestInterfaceSQLDAO_CreateMultiple(t *testing.T) { for i, iface := range got { assert.NotEqual(t, uuid.Nil, iface.ID) assert.Equal(t, tc.inputs[i].InstanceID, iface.InstanceID, "result order should match input order") + assert.Equal(t, tc.inputs[i].RoutingProfile, iface.RoutingProfile) assert.Equal(t, tc.inputs[i].Status, iface.Status) assert.NotZero(t, iface.Created) assert.NotZero(t, iface.Updated) diff --git a/db/pkg/migrations/20260529120000_interface_routing_profile.go b/db/pkg/migrations/20260529120000_interface_routing_profile.go new file mode 100644 index 000000000..d0752f97a --- /dev/null +++ b/db/pkg/migrations/20260529120000_interface_routing_profile.go @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + tx, terr := db.BeginTx(ctx, &sql.TxOptions{}) + if terr != nil { + handlePanic(terr, "failed to begin transaction") + } + + _, err := tx.ExecContext(ctx, `ALTER TABLE "interface" ADD COLUMN IF NOT EXISTS routing_profile JSONB`) + handleError(tx, err) + + terr = tx.Commit() + if terr != nil { + handlePanic(terr, "failed to commit transaction") + } + + fmt.Print(" [up migration] Added 'routing_profile' column to 'interface' table successfully. ") + return nil + }, func(ctx context.Context, db *bun.DB) error { + if _, err := db.ExecContext(ctx, `ALTER TABLE "interface" DROP COLUMN IF EXISTS routing_profile`); err != nil { + return err + } + fmt.Print(" [down migration] Dropped 'routing_profile' column from 'interface' table successfully. ") + return nil + }) +} diff --git a/docs/index.html b/docs/index.html index 5b0758c2b..f4a8126ec 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5902,8 +5902,10 @@

Typical API Call Flow for Tenant

" class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Must be specified if isPhysical is false

macAddress
string or null
ipAddresses
Array of strings

A list of IPv4 or IPv6 addresses

-
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix based interfaces and is not valid for Subnet based interfaces. The least-significant host bit must be 1.

+
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.

status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"

Status values for Interface objects

created
string <date-time>
updated
string <date-time>
Array of objects (InfiniBandInterface)
Array
id
string <uuid> non-empty
instanceId
string <uuid>
partitionId
string <uuid>
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

When set to true, the Instance will be enabled with the Phone Home service.

object (Labels) <= 10 properties
property name*
additional property
string
Array of objects (InterfaceCreateRequest)

At least one interface must be specified unless autoNetwork is true. Either Subnet or VPC Prefix interfaces allowed. Only one of the Subnets or VPC Prefixes can be attached over Physical interface. If only one Subnet is specified, then it will be attached over physical interface regardless of the value of isPhysical. In case of VPC Prefix, isPhysical will always be true. Mutually exclusive with autoNetwork: when autoNetwork is true this list MUST be empty.

-
Array
subnetId
string <uuid>
vpcPrefixId
string <uuid>
ipAddress
string or null

Explicitly requested IP address for the interface. It cannot be specified for Subnet based interfaces. The least-significant host bit must be 1.

+
Array
subnetId
string <uuid>
vpcPrefixId
string <uuid>
ipAddress
string or null

Explicitly requested IP address for the interface. It cannot be specified for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. It cannot be specified for Subnet-based interfaces.

isPhysical
boolean

Specifies whether this Subnet or VPC Prefix should be attached to the Instance over physical interface.

device
string
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Must be specified if isPhysical is false

macAddress
string or null
ipAddresses
Array of strings

A list of IPv4 or IPv6 addresses

-
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix based interfaces and is not valid for Subnet based interfaces. The least-significant host bit must be 1.

+
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.

status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"

Status values for Interface objects

created
string <date-time>
updated
string <date-time>
Array of objects (InfiniBandInterface)
Array
id
string <uuid> non-empty
instanceId
string <uuid>
partitionId
string <uuid>
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

When set to true, the Instances will be enabled with the Phone Home service.

object (Labels) <= 10 properties
property name*
additional property
string
Array of objects (InterfaceCreateRequest)

Interface configuration shared across all instances. At least one interface must be specified unless autoNetwork is true. Either Subnet or VPC Prefix interfaces allowed, only one of the Subnets or VPC Prefixes can be attached over Physical interface. Interface ipAddress is not supported for batch instance creation requests. Mutually exclusive with autoNetwork: when autoNetwork is true this list MUST be empty.

-
Array
subnetId
string <uuid>
vpcPrefixId
string <uuid>
ipAddress
string or null

Explicitly requested IP address for the interface. It cannot be specified for Subnet based interfaces. The least-significant host bit must be 1.

+
Array
subnetId
string <uuid>
vpcPrefixId
string <uuid>
ipAddress
string or null

Explicitly requested IP address for the interface. It cannot be specified for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. It cannot be specified for Subnet-based interfaces.

isPhysical
boolean

Specifies whether this Subnet or VPC Prefix should be attached to the Instance over physical interface.

device
string
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Must be specified if isPhysical is false

macAddress
string or null
ipAddresses
Array of strings

A list of IPv4 or IPv6 addresses

-
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix based interfaces and is not valid for Subnet based interfaces. The least-significant host bit must be 1.

+
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.

status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"

Status values for Interface objects

created
string <date-time>
updated
string <date-time>
Array of objects (InfiniBandInterface)
Array
id
string <uuid> non-empty
instanceId
string <uuid>
partitionId
string <uuid>
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Must be specified if isPhysical is false

macAddress
string or null
ipAddresses
Array of strings

A list of IPv4 or IPv6 addresses

-
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix based interfaces and is not valid for Subnet based interfaces. The least-significant host bit must be 1.

+
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.

status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"

Status values for Interface objects

created
string <date-time>
updated
string <date-time>
Array of objects (InfiniBandInterface)
Array
id
string <uuid> non-empty
instanceId
string <uuid>
partitionId
string <uuid>
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

IDs of additional VPCs the Instance should attach to through non-primary interfaces. This field may only be specified when every entry in interfaces uses vpcPrefixId. IDs must be unique, must be valid UUIDs, and must not include the primary vpcId.

Array of objects (InterfaceCreateRequest)

Update Interfaces of the Instance. Mutually exclusive with autoNetwork: when autoNetwork is true this list MUST be empty.

-
Array
subnetId
string <uuid>
vpcPrefixId
string <uuid>
ipAddress
string or null

Explicitly requested IP address for the interface. It cannot be specified for Subnet based interfaces. The least-significant host bit must be 1.

+
Array
subnetId
string <uuid>
vpcPrefixId
string <uuid>
ipAddress
string or null

Explicitly requested IP address for the interface. It cannot be specified for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. It cannot be specified for Subnet-based interfaces.

isPhysical
boolean

Specifies whether this Subnet or VPC Prefix should be attached to the Instance over physical interface.

device
string
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Must be specified if isPhysical is false

macAddress
string or null
ipAddresses
Array of strings

A list of IPv4 or IPv6 addresses

-
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix based interfaces and is not valid for Subnet based interfaces. The least-significant host bit must be 1.

+
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.

status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"

Status values for Interface objects

created
string <date-time>
updated
string <date-time>
Array of objects (InfiniBandInterface)
Array
id
string <uuid> non-empty
instanceId
string <uuid>
partitionId
string <uuid>
Typical API Call Flow for Tenant " class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg">

Must be specified if isPhysical is false

macAddress
string or null
ipAddresses
Array of strings

A list of IPv4 or IPv6 addresses

-
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix based interfaces and is not valid for Subnet based interfaces. The least-significant host bit must be 1.

-
status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"
requestedIpAddress
string or null

Explicitly requested IP address for the interface. This is only used for VPC Prefix-based interfaces and is not valid for Subnet-based interfaces. The least-significant host bit must be 1.

+
InterfaceRoutingProfile (object) or null

Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.

+
One of
allowedAnycastPrefixes
Array of strings
Default: []

CIDR prefixes this interface is allowed to announce as anycast routes.

+
status
string (InterfaceStatus)
Enum: "Pending" "Provisioning" "Ready" "Deleting" "Error"

Status values for Interface objects

created
string <date-time>
updated
string <date-time>