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 @@
Must be specified if isPhysical is false
A list of IPv4 or IPv6 addresses
-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.
+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.
+Interface-local routing profile options. Only valid for VPC Prefix-based interfaces.
Status values for Interface objects
| 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Array of objects (InterfaceCreateRequest) At least one interface must be specified unless | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Array
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||