Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

DROP INDEX IF EXISTS rack_external_id_idx;

ALTER TABLE rack
DROP COLUMN IF EXISTS external_id;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

-- Adds the Core-side stable rack identifier (ExpectedRack.rack_id, e.g.
-- "a12") so the new expected-inventory mirror can match Flow racks against
-- Core unambiguously and idempotently. The column is nullable: racks created
-- before the mirror runs (or via the legacy ingestion gRPC) start without it
-- and are adopted on the first sync that finds a Core match by
-- (manufacturer, serial_number). The partial unique index leaves NULL rows
-- unconstrained but rejects duplicate external_id assignments.
ALTER TABLE rack
ADD COLUMN external_id TEXT;

CREATE UNIQUE INDEX rack_external_id_idx
ON rack (external_id)
WHERE external_id IS NOT NULL;
19 changes: 12 additions & 7 deletions rest-api/flow/internal/db/model/rack.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@ type Rack struct {
Description map[string]any `bun:"description,type:jsonb,json_use_number"`
Location map[string]any `bun:"location,type:jsonb"`
NVLDomainID uuid.UUID `bun:"nvldomain_id,type:uuid"`
Status RackStatus `bun:"status,type:varchar(16),default:'new'"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
IngestedAt *time.Time `bun:"ingested_at"`
DeletedAt *time.Time `bun:"deleted_at,soft_delete"`
Components []Component `bun:"rel:has-many,join:id=rack_id"`
NVLDomain *NVLDomain `bun:"rel:belongs-to,join:nvldomain_id=id"`
// ExternalID is Core's operator-supplied stable rack identifier
// (ExpectedRack.rack_id, e.g. "a12") populated by the expected-inventory
// mirror. NULL on racks that the mirror has never adopted (e.g. legacy
// ingestion-gRPC rows on first run).
ExternalID *string `bun:"external_id"`
Status RackStatus `bun:"status,type:varchar(16),default:'new'"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
IngestedAt *time.Time `bun:"ingested_at"`
DeletedAt *time.Time `bun:"deleted_at,soft_delete"`
Components []Component `bun:"rel:has-many,join:id=rack_id"`
NVLDomain *NVLDomain `bun:"rel:belongs-to,join:nvldomain_id=id"`
}

type RackStatus string
Expand Down
245 changes: 245 additions & 0 deletions rest-api/flow/internal/nicoapi/expecteddetails_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package nicoapi

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

pb "github.com/NVIDIA/infra-controller/rest-api/flow/internal/nicoapi/gen"
)

func TestExpectedRackDetailFromPb(t *testing.T) {
t.Run("full metadata + rack ids", func(t *testing.T) {
er := &pb.ExpectedRack{
RackId: &pb.RackId{Id: "a12"},
RackProfileId: &pb.RackProfileId{Id: "gb200-nvl72"},
Metadata: &pb.Metadata{
Name: "Rack A12",
Description: "Building 1, Row 3",
Labels: []*pb.Label{
labelKV("chassis.manufacturer", "Foxconn"),
labelKV("chassis.serial-number", "SN12345"),
labelKV("location.datacenter", "DC-East"),
},
},
}

got := expectedRackDetailFromPb(er)

assert.Equal(t, "a12", got.RackID)
assert.Equal(t, "gb200-nvl72", got.RackProfileID)
assert.Equal(t, "Rack A12", got.Name)
assert.Equal(t, "Building 1, Row 3", got.Description)
assert.Equal(t, map[string]string{
"chassis.manufacturer": "Foxconn",
"chassis.serial-number": "SN12345",
"location.datacenter": "DC-East",
}, got.Labels)
})

t.Run("missing optional rack_id stays empty", func(t *testing.T) {
er := &pb.ExpectedRack{
RackProfileId: &pb.RackProfileId{Id: "gb200-nvl72"},
}
got := expectedRackDetailFromPb(er)
assert.Empty(t, got.RackID)
assert.Equal(t, "gb200-nvl72", got.RackProfileID)
assert.Nil(t, got.Labels)
})
}

func TestExpectedMachineDetailFromPb(t *testing.T) {
t.Run("full proto", func(t *testing.T) {
em := &pb.ExpectedMachine{
Id: &pb.UUID{Value: "11111111-1111-1111-1111-111111111111"},
BmcMacAddress: "aa:bb:cc:dd:ee:01",
BmcIpAddress: strPtr("10.0.0.1"),
ChassisSerialNumber: "CSN-001",
RackId: &pb.RackId{Id: "a12"},
Metadata: &pb.Metadata{
Name: "host-001",
Description: "compute node",
Labels: []*pb.Label{
labelKV("manufacturer", "Supermicro"),
labelKV("model", "ARS-211GL-NHR"),
labelKV("firmware_version", "1.2.3"),
labelKV("slot_id", "1"),
labelKV("tray_idx", "2"),
labelKV("host_id", "3"),
},
},
}

got := expectedMachineDetailFromPb(em)

assert.Equal(t, "11111111-1111-1111-1111-111111111111", got.ExpectedMachineID)
assert.Equal(t, "aa:bb:cc:dd:ee:01", got.BMCMACAddress)
assert.Equal(t, "10.0.0.1", got.BMCIPAddress)
assert.Equal(t, "CSN-001", got.ChassisSerialNumber)
assert.Equal(t, "a12", got.RackID)
assert.Equal(t, "host-001", got.Name)
assert.Equal(t, "compute node", got.Description)
require.NotNil(t, got.Labels)
assert.Equal(t, "Supermicro", got.Labels["manufacturer"])
assert.Equal(t, "1.2.3", got.Labels["firmware_version"])
assert.Equal(t, "1", got.Labels["slot_id"])
})

t.Run("missing optional fields stay empty", func(t *testing.T) {
em := &pb.ExpectedMachine{
BmcMacAddress: "aa:bb:cc:dd:ee:02",
ChassisSerialNumber: "CSN-002",
}
got := expectedMachineDetailFromPb(em)
assert.Empty(t, got.ExpectedMachineID)
assert.Empty(t, got.BMCIPAddress)
assert.Empty(t, got.RackID)
assert.Nil(t, got.Labels)
})
}

func TestExpectedSwitchDetailFromPb(t *testing.T) {
es := &pb.ExpectedSwitch{
ExpectedSwitchId: &pb.UUID{Value: "22222222-2222-2222-2222-222222222222"},
BmcMacAddress: "aa:bb:cc:dd:ee:11",
BmcIpAddress: "10.0.0.11",
SwitchSerialNumber: "SSN-001",
RackId: &pb.RackId{Id: "a12"},
Metadata: &pb.Metadata{
Name: "switch-001",
Labels: []*pb.Label{
labelKV("manufacturer", "NVIDIA"),
labelKV("model", "Q3450-LD"),
},
},
}

got := expectedSwitchDetailFromPb(es)

assert.Equal(t, "22222222-2222-2222-2222-222222222222", got.ExpectedSwitchID)
assert.Equal(t, "aa:bb:cc:dd:ee:11", got.BMCMACAddress)
assert.Equal(t, "10.0.0.11", got.BMCIPAddress)
assert.Equal(t, "SSN-001", got.SwitchSerialNumber)
assert.Equal(t, "a12", got.RackID)
assert.Equal(t, "switch-001", got.Name)
assert.Equal(t, "NVIDIA", got.Labels["manufacturer"])
}

func TestExpectedPowerShelfDetailFromPb(t *testing.T) {
eps := &pb.ExpectedPowerShelf{
ExpectedPowerShelfId: &pb.UUID{Value: "33333333-3333-3333-3333-333333333333"},
BmcMacAddress: "aa:bb:cc:dd:ee:21",
BmcIpAddress: "10.0.0.21",
ShelfSerialNumber: "PSN-001",
RackId: &pb.RackId{Id: "a12"},
Metadata: &pb.Metadata{
Name: "shelf-001",
Labels: []*pb.Label{
labelKV("manufacturer", "Lite-On"),
},
},
}

got := expectedPowerShelfDetailFromPb(eps)

assert.Equal(t, "33333333-3333-3333-3333-333333333333", got.ExpectedPowerShelfID)
assert.Equal(t, "aa:bb:cc:dd:ee:21", got.BMCMACAddress)
assert.Equal(t, "10.0.0.21", got.BMCIPAddress)
assert.Equal(t, "PSN-001", got.ShelfSerialNumber)
assert.Equal(t, "a12", got.RackID)
assert.Equal(t, "shelf-001", got.Name)
assert.Equal(t, "Lite-On", got.Labels["manufacturer"])
}

func TestMetadataToGoNilSafe(t *testing.T) {
name, desc, labels := metadataToGo(nil)
assert.Empty(t, name)
assert.Empty(t, desc)
assert.Nil(t, labels)
}

func TestMetadataToGoSkipsValueNilLabels(t *testing.T) {
md := &pb.Metadata{
Labels: []*pb.Label{
{Key: "with-value", Value: strPtr("v")},
{Key: "no-value"},
nil,
},
}
_, _, labels := metadataToGo(md)
assert.Equal(t, map[string]string{"with-value": "v"}, labels)
}

func TestMockGetAllExpectedDetailsRoundTrip(t *testing.T) {
ctx := context.Background()
c := NewMockClient()

c.AddExpectedRackDetail(ExpectedRackDetail{RackID: "a12", RackProfileID: "gb200"})
c.AddExpectedRackDetail(ExpectedRackDetail{RackID: "b13", RackProfileID: "gb200"})
c.AddExpectedMachineDetail(ExpectedMachineDetail{
ExpectedMachineID: "uuid-m1", ChassisSerialNumber: "CSN-1", RackID: "a12",
})
c.AddExpectedSwitchDetail(ExpectedSwitchDetail{
ExpectedSwitchID: "uuid-s1", SwitchSerialNumber: "SSN-1", RackID: "a12",
})
c.AddExpectedPowerShelfDetail(ExpectedPowerShelfDetail{
ExpectedPowerShelfID: "uuid-p1", ShelfSerialNumber: "PSN-1", RackID: "a12",
})

racks, err := c.GetAllExpectedRackDetails(ctx)
require.NoError(t, err)
assert.Len(t, racks, 2)

machines, err := c.GetAllExpectedMachineDetails(ctx)
require.NoError(t, err)
assert.Len(t, machines, 1)
assert.Equal(t, "uuid-m1", machines[0].ExpectedMachineID)

switches, err := c.GetAllExpectedSwitchDetails(ctx)
require.NoError(t, err)
assert.Len(t, switches, 1)

shelves, err := c.GetAllExpectedPowerShelfDetails(ctx)
require.NoError(t, err)
assert.Len(t, shelves, 1)
}

func TestMockGetAllExpectedDetailsEmptyReturnsNil(t *testing.T) {
ctx := context.Background()
c := NewMockClient()

for _, fn := range []func() (int, error){
func() (int, error) {
r, err := c.GetAllExpectedRackDetails(ctx)
return len(r), err
},
func() (int, error) {
r, err := c.GetAllExpectedMachineDetails(ctx)
return len(r), err
},
func() (int, error) {
r, err := c.GetAllExpectedSwitchDetails(ctx)
return len(r), err
},
func() (int, error) {
r, err := c.GetAllExpectedPowerShelfDetails(ctx)
return len(r), err
},
} {
n, err := fn()
assert.NoError(t, err)
assert.Zero(t, n)
}
}

func labelKV(k, v string) *pb.Label {
val := v
return &pb.Label{Key: k, Value: &val}
}

func strPtr(s string) *string { return &s }
92 changes: 92 additions & 0 deletions rest-api/flow/internal/nicoapi/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,82 @@ func (c *grpcClient) GetAllExpectedPowerShelvesLinked(ctx context.Context) ([]Li
return results, nil
}

func (c *grpcClient) GetAllExpectedRackDetails(ctx context.Context) ([]ExpectedRackDetail, error) {
ctx, cancel := context.WithTimeout(ctx, c.grpcTimeout)
defer cancel()

resp, err := c.gclient.GetAllExpectedRacks(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("failed to get all expected racks: %w", err)
}
rows := resp.GetExpectedRacks()
if len(rows) == 0 {
return nil, nil
}
results := make([]ExpectedRackDetail, 0, len(rows))
for _, er := range rows {
results = append(results, expectedRackDetailFromPb(er))
}
return results, nil
}

func (c *grpcClient) GetAllExpectedMachineDetails(ctx context.Context) ([]ExpectedMachineDetail, error) {
ctx, cancel := context.WithTimeout(ctx, c.grpcTimeout)
defer cancel()

resp, err := c.gclient.GetAllExpectedMachines(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("failed to get all expected machines: %w", err)
}
rows := resp.GetExpectedMachines()
if len(rows) == 0 {
return nil, nil
}
results := make([]ExpectedMachineDetail, 0, len(rows))
for _, em := range rows {
results = append(results, expectedMachineDetailFromPb(em))
}
return results, nil
}

func (c *grpcClient) GetAllExpectedSwitchDetails(ctx context.Context) ([]ExpectedSwitchDetail, error) {
ctx, cancel := context.WithTimeout(ctx, c.grpcTimeout)
defer cancel()

resp, err := c.gclient.GetAllExpectedSwitches(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("failed to get all expected switches: %w", err)
}
rows := resp.GetExpectedSwitches()
if len(rows) == 0 {
return nil, nil
}
results := make([]ExpectedSwitchDetail, 0, len(rows))
for _, es := range rows {
results = append(results, expectedSwitchDetailFromPb(es))
}
return results, nil
}

func (c *grpcClient) GetAllExpectedPowerShelfDetails(ctx context.Context) ([]ExpectedPowerShelfDetail, error) {
ctx, cancel := context.WithTimeout(ctx, c.grpcTimeout)
defer cancel()

resp, err := c.gclient.GetAllExpectedPowerShelves(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("failed to get all expected power shelves: %w", err)
}
rows := resp.GetExpectedPowerShelves()
if len(rows) == 0 {
return nil, nil
}
results := make([]ExpectedPowerShelfDetail, 0, len(rows))
for _, eps := range rows {
results = append(results, expectedPowerShelfDetailFromPb(eps))
}
return results, nil
}

func (c *grpcClient) GetDesiredFirmwareVersions(ctx context.Context) ([]*pb.DesiredFirmwareVersionEntry, error) {
ctx, cancel := context.WithTimeout(ctx, c.grpcTimeout)
defer cancel()
Expand Down Expand Up @@ -843,3 +919,19 @@ func (c *grpcClient) SetPowerShelfControllerState(shelfID, state string) {
func (c *grpcClient) SetRackHostMachineIDs(rackID string, machineIDs []string) {
panic("Not a unit test")
}

func (c *grpcClient) AddExpectedRackDetail(detail ExpectedRackDetail) {
panic("Not a unit test")
}

func (c *grpcClient) AddExpectedMachineDetail(detail ExpectedMachineDetail) {
panic("Not a unit test")
}

func (c *grpcClient) AddExpectedSwitchDetail(detail ExpectedSwitchDetail) {
panic("Not a unit test")
}

func (c *grpcClient) AddExpectedPowerShelfDetail(detail ExpectedPowerShelfDetail) {
panic("Not a unit test")
}
Loading
Loading