diff --git a/api/pkg/api/handler/rack.go b/api/pkg/api/handler/rack.go index 51983cca..067b4814 100644 --- a/api/pkg/api/handler/rack.go +++ b/api/pkg/api/handler/rack.go @@ -27,9 +27,11 @@ import ( "slices" "strings" + "github.com/google/uuid" "github.com/labstack/echo/v4" "go.opentelemetry.io/otel/attribute" temporalEnums "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" tClient "go.temporal.io/sdk/client" tp "go.temporal.io/sdk/temporal" @@ -433,6 +435,332 @@ func (garh GetAllRackHandler) Handle(c echo.Context) error { return c.JSON(http.StatusOK, apiRacks) } +// ~~~~~ Create Rack Handler ~~~~~ // + +// CreateRackHandler is the API Handler for creating a new expected Rack +type CreateRackHandler struct { + dbSession *cdb.Session + tc tClient.Client + scp *sc.ClientPool + cfg *config.Config + tracerSpan *cutil.TracerSpan +} + +// NewCreateRackHandler initializes and returns a new handler for creating a Rack +func NewCreateRackHandler(dbSession *cdb.Session, tc tClient.Client, scp *sc.ClientPool, cfg *config.Config) CreateRackHandler { + return CreateRackHandler{ + dbSession: dbSession, + tc: tc, + scp: scp, + cfg: cfg, + tracerSpan: cutil.NewTracerSpan(), + } +} + +// Handle godoc +// @Summary Create a Rack +// @Description Create a new expected Rack definition in RLA +// @Tags rack +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param org path string true "Name of NGC organization" +// @Param body body model.APICreateRackRequest true "Create rack request" +// @Success 201 {object} model.APICreateRackResponse +// @Router /v2/org/{org}/carbide/rack [post] +func (crh CreateRackHandler) Handle(c echo.Context) error { + org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("Rack", "Create", c, crh.tracerSpan) + if handlerSpan != nil { + defer handlerSpan.End() + } + + // Parse and validate request body + var apiRequest model.APICreateRackRequest + if err := c.Bind(&apiRequest); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to parse request data", nil) + } + if err := apiRequest.Validate(); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, err.Error(), nil) + } + + // Is DB user missing? + if dbUser == nil { + logger.Error().Msg("invalid User object found in request context") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil) + } + + // Validate org membership + ok, err := auth.ValidateOrgMembership(dbUser, org) + if !ok { + if err != nil { + logger.Error().Err(err).Msg("error validating org membership for User in request") + } else { + logger.Warn().Msg("could not validate org membership for user, access denied") + } + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Failed to validate membership for org: %s", org), nil) + } + + // Validate role, only Provider Admins are allowed to create Rack + ok = auth.ValidateUserRoles(dbUser, org, nil, auth.ProviderAdminRole) + if !ok { + logger.Warn().Msg("user does not have Provider Admin role, access denied") + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Provider Admin role with org", nil) + } + + // Get Infrastructure Provider for org + infrastructureProvider, err := common.GetInfrastructureProviderForOrg(ctx, nil, crh.dbSession, org) + if err != nil { + logger.Warn().Err(err).Msg("error getting infrastructure provider for org") + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to retrieve Infrastructure Provider for org", nil) + } + + // Validate the site + site, err := common.GetSiteFromIDString(ctx, nil, apiRequest.SiteID, crh.dbSession) + if err != nil { + if errors.Is(err, common.ErrInvalidID) { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to validate Site specified in request: invalid ID", nil) + } + if errors.Is(err, cdb.ErrDoesNotExist) { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Site specified in request does not exist", nil) + } + logger.Error().Err(err).Msg("error retrieving Site from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Site specified in request due to DB error", nil) + } + + // Verify site belongs to the org's Infrastructure Provider + if site.InfrastructureProviderID != infrastructureProvider.ID { + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Site specified in request doesn't belong to current org's Provider", nil) + } + + siteConfig := &cdbm.SiteConfig{} + if site.Config != nil { + siteConfig = site.Config + } + + if !siteConfig.RackLevelAdministration { + logger.Warn().Msg("site does not have Rack Level Administration enabled") + return cutil.NewAPIErrorResponse(c, http.StatusPreconditionFailed, "Site does not have Rack Level Administration enabled", nil) + } + + // Get the temporal client for the site + stc, err := crh.scp.GetClientByID(site.ID) + if err != nil { + logger.Error().Err(err).Msg("failed to retrieve Temporal client for Site") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve client for Site", nil) + } + + // Build RLA request + rlaRequest := &rlav1.CreateExpectedRackRequest{ + Rack: apiRequest.ToProtoRack(), + } + + // Execute workflow + workflowOptions := tClient.StartWorkflowOptions{ + ID: fmt.Sprintf("rack-create-%s", apiRequest.SerialNumber), + WorkflowIDReusePolicy: temporalEnums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIDConflictPolicy: temporalEnums.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + WorkflowExecutionTimeout: cutil.WorkflowExecutionTimeout, + TaskQueue: queue.SiteTaskQueue, + } + + ctx, cancel := context.WithTimeout(ctx, cutil.WorkflowContextTimeout) + defer cancel() + + we, err := stc.ExecuteWorkflow(ctx, workflowOptions, "CreateExpectedRack", rlaRequest) + if err != nil { + var alreadyStarted *serviceerror.WorkflowExecutionAlreadyStarted + if errors.As(err, &alreadyStarted) { + return cutil.NewAPIErrorResponse(c, http.StatusConflict, "A rack creation with this serial number is already in progress", nil) + } + logger.Error().Err(err).Msg("failed to execute CreateExpectedRack workflow") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to create Rack", nil) + } + + // Get workflow result + var rlaResponse rlav1.CreateExpectedRackResponse + err = we.Get(ctx, &rlaResponse) + if err != nil { + var timeoutErr *tp.TimeoutError + if errors.As(err, &timeoutErr) || err == context.DeadlineExceeded || ctx.Err() != nil { + return common.TerminateWorkflowOnTimeOut(c, logger, stc, fmt.Sprintf("rack-create-%s", apiRequest.SerialNumber), err, "Rack", "CreateExpectedRack") + } + logger.Error().Err(err).Msg("failed to get result from CreateExpectedRack workflow") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to create Rack", nil) + } + + logger.Info().Str("RackID", rlaResponse.GetId().GetId()).Msg("finishing API handler") + + apiResponse := model.NewAPICreateRackResponse(&rlaResponse) + return c.JSON(http.StatusCreated, apiResponse) +} + +// ~~~~~ Update Rack Handler ~~~~~ // + +// UpdateRackHandler is the API Handler for updating an existing Rack +type UpdateRackHandler struct { + dbSession *cdb.Session + tc tClient.Client + scp *sc.ClientPool + cfg *config.Config + tracerSpan *cutil.TracerSpan +} + +// NewUpdateRackHandler initializes and returns a new handler for updating a Rack +func NewUpdateRackHandler(dbSession *cdb.Session, tc tClient.Client, scp *sc.ClientPool, cfg *config.Config) UpdateRackHandler { + return UpdateRackHandler{ + dbSession: dbSession, + tc: tc, + scp: scp, + cfg: cfg, + tracerSpan: cutil.NewTracerSpan(), + } +} + +// Handle godoc +// @Summary Update a Rack +// @Description Update an existing Rack's fields by ID via RLA +// @Tags rack +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param org path string true "Name of NGC organization" +// @Param id path string true "ID of Rack" +// @Param body body model.APIRackUpdateRequest true "Update rack request" +// @Success 200 {object} model.APIRackUpdateResponse +// @Router /v2/org/{org}/carbide/rack/{id} [patch] +func (urh UpdateRackHandler) Handle(c echo.Context) error { + org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("Rack", "Update", c, urh.tracerSpan) + if handlerSpan != nil { + defer handlerSpan.End() + } + + // Parse and validate request body + var apiRequest model.APIRackUpdateRequest + if err := c.Bind(&apiRequest); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to parse request data", nil) + } + if err := apiRequest.Validate(); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, err.Error(), nil) + } + + // Is DB user missing? + if dbUser == nil { + logger.Error().Msg("invalid User object found in request context") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve current user", nil) + } + + // Validate org membership + ok, err := auth.ValidateOrgMembership(dbUser, org) + if !ok { + if err != nil { + logger.Error().Err(err).Msg("error validating org membership for User in request") + } else { + logger.Warn().Msg("could not validate org membership for user, access denied") + } + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Failed to validate membership for org: %s", org), nil) + } + + // Validate role, only Provider Admins are allowed to update Rack + ok = auth.ValidateUserRoles(dbUser, org, nil, auth.ProviderAdminRole) + if !ok { + logger.Warn().Msg("user does not have Provider Admin role, access denied") + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "User does not have Provider Admin role with org", nil) + } + + // Get Infrastructure Provider for org + infrastructureProvider, err := common.GetInfrastructureProviderForOrg(ctx, nil, urh.dbSession, org) + if err != nil { + logger.Warn().Err(err).Msg("error getting infrastructure provider for org") + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to retrieve Infrastructure Provider for org", nil) + } + + // Get rack ID from URL param + rackStrID := c.Param("id") + if _, err := uuid.Parse(rackStrID); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid rack ID: must be a valid UUID", nil) + } + urh.tracerSpan.SetAttribute(handlerSpan, attribute.String("rack_id", rackStrID), logger) + + // Validate the site + site, err := common.GetSiteFromIDString(ctx, nil, apiRequest.SiteID, urh.dbSession) + if err != nil { + if errors.Is(err, common.ErrInvalidID) { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to validate Site specified in request: invalid ID", nil) + } + if errors.Is(err, cdb.ErrDoesNotExist) { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Site specified in request does not exist", nil) + } + logger.Error().Err(err).Msg("error retrieving Site from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Site specified in request due to DB error", nil) + } + + // Verify site belongs to the org's Infrastructure Provider + if site.InfrastructureProviderID != infrastructureProvider.ID { + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Site specified in request doesn't belong to current org's Provider", nil) + } + + siteConfig := &cdbm.SiteConfig{} + if site.Config != nil { + siteConfig = site.Config + } + + if !siteConfig.RackLevelAdministration { + logger.Warn().Msg("site does not have Rack Level Administration enabled") + return cutil.NewAPIErrorResponse(c, http.StatusPreconditionFailed, "Site does not have Rack Level Administration enabled", nil) + } + + // Get the temporal client for the site + stc, err := urh.scp.GetClientByID(site.ID) + if err != nil { + logger.Error().Err(err).Msg("failed to retrieve Temporal client for Site") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve client for Site", nil) + } + + // Build RLA request + rlaRequest := &rlav1.PatchRackRequest{ + Rack: apiRequest.ToProtoRack(rackStrID), + } + + // Execute workflow + workflowOptions := tClient.StartWorkflowOptions{ + ID: fmt.Sprintf("rack-update-%s", rackStrID), + WorkflowIDReusePolicy: temporalEnums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowIDConflictPolicy: temporalEnums.WORKFLOW_ID_CONFLICT_POLICY_FAIL, + WorkflowExecutionTimeout: cutil.WorkflowExecutionTimeout, + TaskQueue: queue.SiteTaskQueue, + } + + ctx, cancel := context.WithTimeout(ctx, cutil.WorkflowContextTimeout) + defer cancel() + + we, err := stc.ExecuteWorkflow(ctx, workflowOptions, "UpdateRack", rlaRequest) + if err != nil { + var alreadyStarted *serviceerror.WorkflowExecutionAlreadyStarted + if errors.As(err, &alreadyStarted) { + return cutil.NewAPIErrorResponse(c, http.StatusConflict, "A rack update is already in progress for this rack", nil) + } + logger.Error().Err(err).Msg("failed to execute UpdateRack workflow") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to update Rack", nil) + } + + // Get workflow result + var rlaResponse rlav1.PatchRackResponse + err = we.Get(ctx, &rlaResponse) + if err != nil { + var timeoutErr *tp.TimeoutError + if errors.As(err, &timeoutErr) || err == context.DeadlineExceeded || ctx.Err() != nil { + return common.TerminateWorkflowOnTimeOut(c, logger, stc, fmt.Sprintf("rack-update-%s", rackStrID), err, "Rack", "UpdateRack") + } + logger.Error().Err(err).Msg("failed to get result from UpdateRack workflow") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to update Rack", nil) + } + + logger.Info().Str("Report", rlaResponse.GetReport()).Msg("finishing API handler") + + apiResponse := model.NewAPIRackUpdateResponse(&rlaResponse) + return c.JSON(http.StatusOK, apiResponse) +} + // ~~~~~ Validate Rack Handler ~~~~~ // // ValidateRackHandler is the API Handler for validating a Rack's components diff --git a/api/pkg/api/handler/rack_test.go b/api/pkg/api/handler/rack_test.go index 8a0e30bf..519fe0e3 100644 --- a/api/pkg/api/handler/rack_test.go +++ b/api/pkg/api/handler/rack_test.go @@ -148,6 +148,300 @@ func testRackBuildUser(t *testing.T, dbSession *cdb.Session, starfleetID string, return u } +func TestCreateRackHandler_Handle(t *testing.T) { + e := echo.New() + dbSession := testRackInitDB(t) + defer dbSession.Close() + + cfg := common.GetTestConfig() + tcfg, _ := cfg.GetTemporalConfig() + scp := sc.NewClientPool(tcfg) + + org := "test-org" + _, site, _ := testRackSetupTestData(t, dbSession, org) + + siteNoRLA := &cdbm.Site{ + ID: uuid.New(), + Name: "test-site-no-rla", + Org: org, + InfrastructureProviderID: site.InfrastructureProviderID, + Status: cdbm.SiteStatusRegistered, + Config: &cdbm.SiteConfig{}, + } + _, err := dbSession.DB.NewInsert().Model(siteNoRLA).Exec(context.Background()) + assert.Nil(t, err) + + providerUser := testRackBuildUser(t, dbSession, "provider-user-create-rack", org, []string{"FORGE_PROVIDER_ADMIN"}) + tenantUser := testRackBuildUser(t, dbSession, "tenant-user-create-rack", org, []string{"FORGE_TENANT_ADMIN"}) + + handler := NewCreateRackHandler(dbSession, nil, scp, cfg) + + tracer := oteltrace.NewNoopTracerProvider().Tracer("test") + ctx := context.Background() + + newRackID := uuid.NewString() + + tests := []struct { + name string + reqOrg string + user *cdbm.User + body string + mockRackID string + expectedStatus int + }{ + { + name: "success - create rack", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-New","manufacturer":"NVIDIA","serialNumber":"SN-NEW-001"}`, site.ID.String()), + mockRackID: newRackID, + expectedStatus: http.StatusCreated, + }, + { + name: "success - create rack with location", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-Loc","manufacturer":"NVIDIA","serialNumber":"SN-LOC-001","location":{"region":"us-east-1","datacenter":"DC1","room":"A","position":"1"}}`, site.ID.String()), + mockRackID: newRackID, + expectedStatus: http.StatusCreated, + }, + { + name: "failure - missing name", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","manufacturer":"NVIDIA","serialNumber":"SN-001"}`, site.ID.String()), + expectedStatus: http.StatusBadRequest, + }, + { + name: "failure - missing siteId", + reqOrg: org, + user: providerUser, + body: `{"name":"Rack-New","manufacturer":"NVIDIA","serialNumber":"SN-001"}`, + expectedStatus: http.StatusBadRequest, + }, + { + name: "failure - missing manufacturer", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-New","serialNumber":"SN-001"}`, site.ID.String()), + expectedStatus: http.StatusBadRequest, + }, + { + name: "failure - missing serialNumber", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-New","manufacturer":"NVIDIA"}`, site.ID.String()), + expectedStatus: http.StatusBadRequest, + }, + { + name: "failure - RLA not enabled", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-New","manufacturer":"NVIDIA","serialNumber":"SN-001"}`, siteNoRLA.ID.String()), + expectedStatus: http.StatusPreconditionFailed, + }, + { + name: "failure - tenant access denied", + reqOrg: org, + user: tenantUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-New","manufacturer":"NVIDIA","serialNumber":"SN-001"}`, site.ID.String()), + expectedStatus: http.StatusForbidden, + }, + { + name: "failure - invalid siteId", + reqOrg: org, + user: providerUser, + body: fmt.Sprintf(`{"siteId":"%s","name":"Rack-New","manufacturer":"NVIDIA","serialNumber":"SN-001"}`, uuid.NewString()), + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockTemporalClient := &tmocks.Client{} + mockWorkflowRun := &tmocks.WorkflowRun{} + mockWorkflowRun.On("GetID").Return("test-workflow-id") + mockWorkflowRun.Mock.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + resp := args.Get(1).(*rlav1.CreateExpectedRackResponse) + if tt.mockRackID != "" { + resp.Id = &rlav1.UUID{Id: tt.mockRackID} + } + }).Return(nil) + mockTemporalClient.Mock.On("ExecuteWorkflow", mock.Anything, mock.Anything, "CreateExpectedRack", mock.Anything).Return(mockWorkflowRun, nil) + scp.IDClientMap[site.ID.String()] = mockTemporalClient + + path := fmt.Sprintf("/v2/org/%s/carbide/rack", tt.reqOrg) + + req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(tt.body)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + ec := e.NewContext(req, rec) + ec.SetParamNames("orgName") + ec.SetParamValues(tt.reqOrg) + ec.Set("user", tt.user) + + ctx = context.WithValue(ctx, otelecho.TracerKey, tracer) + ec.SetRequest(ec.Request().WithContext(ctx)) + + err := handler.Handle(ec) + + if tt.expectedStatus != rec.Code { + t.Errorf("CreateRackHandler.Handle() status = %v, want %v, response: %v, err: %v", rec.Code, tt.expectedStatus, rec.Body.String(), err) + } + + require.Equal(t, tt.expectedStatus, rec.Code) + if tt.expectedStatus != http.StatusCreated { + return + } + + var apiResp model.APICreateRackResponse + err = json.Unmarshal(rec.Body.Bytes(), &apiResp) + assert.NoError(t, err) + assert.Equal(t, tt.mockRackID, apiResp.ID) + }) + } +} + +func TestUpdateRackHandler_Handle(t *testing.T) { + e := echo.New() + dbSession := testRackInitDB(t) + defer dbSession.Close() + + cfg := common.GetTestConfig() + tcfg, _ := cfg.GetTemporalConfig() + scp := sc.NewClientPool(tcfg) + + org := "test-org" + _, site, _ := testRackSetupTestData(t, dbSession, org) + + siteNoRLA := &cdbm.Site{ + ID: uuid.New(), + Name: "test-site-no-rla", + Org: org, + InfrastructureProviderID: site.InfrastructureProviderID, + Status: cdbm.SiteStatusRegistered, + Config: &cdbm.SiteConfig{}, + } + _, err := dbSession.DB.NewInsert().Model(siteNoRLA).Exec(context.Background()) + assert.Nil(t, err) + + providerUser := testRackBuildUser(t, dbSession, "provider-user-update-rack", org, []string{"FORGE_PROVIDER_ADMIN"}) + tenantUser := testRackBuildUser(t, dbSession, "tenant-user-update-rack", org, []string{"FORGE_TENANT_ADMIN"}) + + handler := NewUpdateRackHandler(dbSession, nil, scp, cfg) + + rackID := uuid.NewString() + + tracer := oteltrace.NewNoopTracerProvider().Tracer("test") + ctx := context.Background() + + tests := []struct { + name string + reqOrg string + user *cdbm.User + rackID string + body string + mockReport string + expectedStatus int + }{ + { + name: "success - update rack name", + reqOrg: org, + user: providerUser, + rackID: rackID, + body: fmt.Sprintf(`{"siteId":"%s","name":"Updated-Rack"}`, site.ID.String()), + mockReport: "Rack updated successfully", + expectedStatus: http.StatusOK, + }, + { + name: "success - update rack location", + reqOrg: org, + user: providerUser, + rackID: rackID, + body: fmt.Sprintf(`{"siteId":"%s","location":{"region":"us-west-2","datacenter":"DC2","room":"B","position":"3"}}`, site.ID.String()), + mockReport: "Rack location updated", + expectedStatus: http.StatusOK, + }, + { + name: "failure - missing siteId", + reqOrg: org, + user: providerUser, + rackID: rackID, + body: `{"name":"Updated-Rack"}`, + expectedStatus: http.StatusBadRequest, + }, + { + name: "failure - RLA not enabled", + reqOrg: org, + user: providerUser, + rackID: rackID, + body: fmt.Sprintf(`{"siteId":"%s","name":"Updated-Rack"}`, siteNoRLA.ID.String()), + expectedStatus: http.StatusPreconditionFailed, + }, + { + name: "failure - tenant access denied", + reqOrg: org, + user: tenantUser, + rackID: rackID, + body: fmt.Sprintf(`{"siteId":"%s","name":"Updated-Rack"}`, site.ID.String()), + expectedStatus: http.StatusForbidden, + }, + { + name: "failure - invalid siteId", + reqOrg: org, + user: providerUser, + rackID: rackID, + body: fmt.Sprintf(`{"siteId":"%s","name":"Updated-Rack"}`, uuid.NewString()), + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockTemporalClient := &tmocks.Client{} + mockWorkflowRun := &tmocks.WorkflowRun{} + mockWorkflowRun.On("GetID").Return("test-workflow-id") + mockWorkflowRun.Mock.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + resp := args.Get(1).(*rlav1.PatchRackResponse) + resp.Report = tt.mockReport + }).Return(nil) + mockTemporalClient.Mock.On("ExecuteWorkflow", mock.Anything, mock.Anything, "UpdateRack", mock.Anything).Return(mockWorkflowRun, nil) + scp.IDClientMap[site.ID.String()] = mockTemporalClient + + path := fmt.Sprintf("/v2/org/%s/carbide/rack/%s", tt.reqOrg, tt.rackID) + + req := httptest.NewRequest(http.MethodPatch, path, strings.NewReader(tt.body)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + ec := e.NewContext(req, rec) + ec.SetParamNames("orgName", "id") + ec.SetParamValues(tt.reqOrg, tt.rackID) + ec.Set("user", tt.user) + + ctx = context.WithValue(ctx, otelecho.TracerKey, tracer) + ec.SetRequest(ec.Request().WithContext(ctx)) + + err := handler.Handle(ec) + + if tt.expectedStatus != rec.Code { + t.Errorf("UpdateRackHandler.Handle() status = %v, want %v, response: %v, err: %v", rec.Code, tt.expectedStatus, rec.Body.String(), err) + } + + require.Equal(t, tt.expectedStatus, rec.Code) + if tt.expectedStatus != http.StatusOK { + return + } + + var apiResp model.APIRackUpdateResponse + err = json.Unmarshal(rec.Body.Bytes(), &apiResp) + assert.NoError(t, err) + assert.Equal(t, tt.mockReport, apiResp.Report) + }) + } +} + func TestGetRackHandler_Handle(t *testing.T) { // Setup e := echo.New() diff --git a/api/pkg/api/model/rack.go b/api/pkg/api/model/rack.go index c2239a80..f1079112 100644 --- a/api/pkg/api/model/rack.go +++ b/api/pkg/api/model/rack.go @@ -21,6 +21,9 @@ import ( "fmt" "net/url" + validation "github.com/go-ozzo/ozzo-validation/v4" + validationis "github.com/go-ozzo/ozzo-validation/v4/is" + rlav1 "github.com/nvidia/bare-metal-manager-rest/workflow-schema/rla/protobuf/v1" ) @@ -515,6 +518,351 @@ func NewAPIRackValidationResult(protoResp *rlav1.ValidateComponentsResponse) *AP return result } +// ========== Create Rack Request/Response ========== + +// APICreateRackRequest is the JSON body for POST /rack (CreateExpectedRack) +type APICreateRackRequest struct { + SiteID string `json:"siteId"` + Name string `json:"name"` + Manufacturer string `json:"manufacturer"` + Model *string `json:"model"` + SerialNumber string `json:"serialNumber"` + Description *string `json:"description"` + Location *APIRackLocation `json:"location"` + Components []*APICreateRackComponentRequest `json:"components"` +} + +// Validate validates the create rack request +func (r *APICreateRackRequest) Validate() error { + err := validation.ValidateStruct(r, + validation.Field(&r.SiteID, + validation.Required.Error(validationErrorValueRequired), + validationis.UUID.Error(validationErrorInvalidUUID)), + validation.Field(&r.Name, + validation.Required.Error(validationErrorValueRequired)), + validation.Field(&r.Manufacturer, + validation.Required.Error(validationErrorValueRequired)), + validation.Field(&r.SerialNumber, + validation.Required.Error(validationErrorValueRequired)), + ) + if err != nil { + return err + } + errs := validation.Errors{} + for i, comp := range r.Components { + if comp == nil { + errs[fmt.Sprintf("components[%d]", i)] = fmt.Errorf("must not be null") + continue + } + if compErr := comp.Validate(); compErr != nil { + errs[fmt.Sprintf("components[%d]", i)] = compErr + } + } + if len(errs) > 0 { + return errs + } + return nil +} + +// ToProtoRack converts the API request to an RLA Rack proto +func (r *APICreateRackRequest) ToProtoRack() *rlav1.Rack { + rack := &rlav1.Rack{ + Info: &rlav1.DeviceInfo{ + Name: r.Name, + Manufacturer: r.Manufacturer, + SerialNumber: r.SerialNumber, + }, + } + if r.Model != nil { + rack.Info.Model = r.Model + } + if r.Description != nil { + rack.Info.Description = r.Description + } + if r.Location != nil { + rack.Location = r.Location.ToProto() + } + for _, comp := range r.Components { + rack.Components = append(rack.Components, comp.ToProto()) + } + return rack +} + +// APICreateRackComponentRequest is a component in a create rack request +type APICreateRackComponentRequest struct { + Type string `json:"type"` + Name string `json:"name"` + Manufacturer string `json:"manufacturer"` + Model *string `json:"model"` + SerialNumber string `json:"serialNumber"` + Description *string `json:"description"` + FirmwareVersion *string `json:"firmwareVersion"` + ComponentID *string `json:"componentId"` + SlotID *int32 `json:"slotId"` + TrayIdx *int32 `json:"trayIdx"` + HostID *int32 `json:"hostId"` + BMCs []*APICreateRackBMCRequest `json:"bmcs"` +} + +// Validate validates a create rack component +func (c *APICreateRackComponentRequest) Validate() error { + err := validation.ValidateStruct(c, + validation.Field(&c.Type, + validation.Required.Error(validationErrorValueRequired)), + validation.Field(&c.Name, + validation.Required.Error(validationErrorValueRequired)), + validation.Field(&c.SerialNumber, + validation.Required.Error(validationErrorValueRequired)), + ) + if err != nil { + return err + } + if _, ok := rlav1.ComponentType_value[c.Type]; !ok { + return fmt.Errorf("invalid component type: %q", c.Type) + } + errs := validation.Errors{} + for i, bmc := range c.BMCs { + if bmc == nil { + errs[fmt.Sprintf("bmcs[%d]", i)] = fmt.Errorf("must not be null") + continue + } + if bmcErr := bmc.Validate(); bmcErr != nil { + errs[fmt.Sprintf("bmcs[%d]", i)] = bmcErr + } + } + if len(errs) > 0 { + return errs + } + return nil +} + +// ToProto converts to RLA Component proto +func (c *APICreateRackComponentRequest) ToProto() *rlav1.Component { + comp := &rlav1.Component{ + Type: componentTypeFromString(c.Type), + Info: &rlav1.DeviceInfo{ + Name: c.Name, + Manufacturer: c.Manufacturer, + SerialNumber: c.SerialNumber, + }, + } + if c.SlotID != nil || c.TrayIdx != nil || c.HostID != nil { + comp.Position = &rlav1.RackPosition{} + if c.SlotID != nil { + comp.Position.SlotId = *c.SlotID + } + if c.TrayIdx != nil { + comp.Position.TrayIdx = *c.TrayIdx + } + if c.HostID != nil { + comp.Position.HostId = *c.HostID + } + } + if c.Model != nil { + comp.Info.Model = c.Model + } + if c.Description != nil { + comp.Info.Description = c.Description + } + if c.FirmwareVersion != nil { + comp.FirmwareVersion = *c.FirmwareVersion + } + if c.ComponentID != nil { + comp.ComponentId = *c.ComponentID + } + for _, bmc := range c.BMCs { + comp.Bmcs = append(comp.Bmcs, bmc.ToProto()) + } + return comp +} + +// APICreateRackBMCRequest is a BMC entry in a create rack request +type APICreateRackBMCRequest struct { + Type string `json:"type"` + MacAddress string `json:"macAddress"` + IPAddress *string `json:"ipAddress"` + User *string `json:"user"` + Password *string `json:"password"` +} + +// Validate validates a create rack BMC +func (b *APICreateRackBMCRequest) Validate() error { + err := validation.ValidateStruct(b, + validation.Field(&b.Type, + validation.Required.Error(validationErrorValueRequired)), + validation.Field(&b.MacAddress, + validation.Required.Error(validationErrorValueRequired)), + ) + if err != nil { + return err + } + if _, ok := rlav1.BMCType_value[b.Type]; !ok { + return fmt.Errorf("invalid bmc type: %q", b.Type) + } + return nil +} + +// ToProto converts to RLA BMCInfo proto +func (b *APICreateRackBMCRequest) ToProto() *rlav1.BMCInfo { + bmc := &rlav1.BMCInfo{ + Type: bmcTypeFromString(b.Type), + MacAddress: b.MacAddress, + } + if b.IPAddress != nil { + bmc.IpAddress = b.IPAddress + } + if b.User != nil { + bmc.User = b.User + } + if b.Password != nil { + bmc.Password = b.Password + } + return bmc +} + +// ToProto converts APIRackLocation to RLA Location proto +func (arl *APIRackLocation) ToProto() *rlav1.Location { + return &rlav1.Location{ + Region: arl.Region, + Datacenter: arl.Datacenter, + Room: arl.Room, + Position: arl.Position, + } +} + +// APICreateRackResponse is the API response for POST /rack +type APICreateRackResponse struct { + ID string `json:"id"` +} + +// NewAPICreateRackResponse creates a response from the RLA CreateExpectedRackResponse +func NewAPICreateRackResponse(resp *rlav1.CreateExpectedRackResponse) *APICreateRackResponse { + if resp == nil { + return &APICreateRackResponse{} + } + return &APICreateRackResponse{ + ID: resp.GetId().GetId(), + } +} + +// ========== Update Rack Request/Response ========== + +// APIRackUpdateRequest is the JSON body for PATCH /rack/:id (PatchRack) +type APIRackUpdateRequest struct { + SiteID string `json:"siteId"` + Name *string `json:"name"` + Manufacturer *string `json:"manufacturer"` + Model *string `json:"model"` + SerialNumber *string `json:"serialNumber"` + Description *string `json:"description"` + Location *APIRackUpdateLocation `json:"location"` +} + +// APIRackUpdateLocation is a patch-specific location type with pointer fields +// to distinguish between "not provided" and "set to empty string". +type APIRackUpdateLocation struct { + Region *string `json:"region"` + Datacenter *string `json:"datacenter"` + Room *string `json:"room"` + Position *string `json:"position"` +} + +func (l *APIRackUpdateLocation) ToProto() *rlav1.Location { + loc := &rlav1.Location{} + if l.Region != nil { + loc.Region = *l.Region + } + if l.Datacenter != nil { + loc.Datacenter = *l.Datacenter + } + if l.Room != nil { + loc.Room = *l.Room + } + if l.Position != nil { + loc.Position = *l.Position + } + return loc +} + +// Validate validates the update rack request +func (r *APIRackUpdateRequest) Validate() error { + err := validation.ValidateStruct(r, + validation.Field(&r.SiteID, + validation.Required.Error(validationErrorValueRequired), + validationis.UUID.Error(validationErrorInvalidUUID)), + ) + if err != nil { + return err + } + if r.Name == nil && r.Manufacturer == nil && r.Model == nil && + r.SerialNumber == nil && r.Description == nil && r.Location == nil { + return fmt.Errorf("at least one field to update must be provided") + } + return nil +} + +// ToProtoRack converts the API update request to an RLA Rack proto. +// The rack ID is set from the URL path parameter. +func (r *APIRackUpdateRequest) ToProtoRack(rackID string) *rlav1.Rack { + rack := &rlav1.Rack{ + Info: &rlav1.DeviceInfo{ + Id: &rlav1.UUID{Id: rackID}, + }, + } + if r.Name != nil { + rack.Info.Name = *r.Name + } + if r.Manufacturer != nil { + rack.Info.Manufacturer = *r.Manufacturer + } + if r.Model != nil { + rack.Info.Model = r.Model + } + if r.SerialNumber != nil { + rack.Info.SerialNumber = *r.SerialNumber + } + if r.Description != nil { + rack.Info.Description = r.Description + } + if r.Location != nil { + rack.Location = r.Location.ToProto() + } + return rack +} + +// APIRackUpdateResponse is the API response for PATCH /rack/:id +type APIRackUpdateResponse struct { + Report string `json:"report"` +} + +// NewAPIRackUpdateResponse creates a response from the RLA PatchRackResponse +func NewAPIRackUpdateResponse(resp *rlav1.PatchRackResponse) *APIRackUpdateResponse { + if resp == nil { + return &APIRackUpdateResponse{} + } + return &APIRackUpdateResponse{ + Report: resp.GetReport(), + } +} + +// ========== Proto Enum Helpers ========== + +// componentTypeFromString converts a string to RLA ComponentType enum +func componentTypeFromString(s string) rlav1.ComponentType { + if v, ok := rlav1.ComponentType_value[s]; ok { + return rlav1.ComponentType(v) + } + return rlav1.ComponentType_COMPONENT_TYPE_UNKNOWN +} + +// bmcTypeFromString converts a string to RLA BMCType enum +func bmcTypeFromString(s string) rlav1.BMCType { + if v, ok := rlav1.BMCType_value[s]; ok { + return rlav1.BMCType(v) + } + return rlav1.BMCType_BMC_TYPE_UNKNOWN +} + // ========== Bring Up Request ========== // APIBringUpRackRequest is the request body for bring up operations on a single rack diff --git a/api/pkg/api/model/rack_test.go b/api/pkg/api/model/rack_test.go index 2632bee7..63c37dbc 100644 --- a/api/pkg/api/model/rack_test.go +++ b/api/pkg/api/model/rack_test.go @@ -205,6 +205,225 @@ func TestNewAPIRack(t *testing.T) { } } +func TestAPICreateRackRequest_Validate(t *testing.T) { + tests := []struct { + name string + request APICreateRackRequest + wantErr bool + }{ + { + name: "valid - all required fields", + request: APICreateRackRequest{ + SiteID: "550e8400-e29b-41d4-a716-446655440000", + Name: "Rack-01", + Manufacturer: "NVIDIA", + SerialNumber: "SN-001", + }, + wantErr: false, + }, + { + name: "valid - with optional fields", + request: APICreateRackRequest{ + SiteID: "550e8400-e29b-41d4-a716-446655440000", + Name: "Rack-01", + Manufacturer: "NVIDIA", + SerialNumber: "SN-001", + Model: strPtr("NVL72"), + Description: strPtr("Test rack"), + Location: &APIRackLocation{Region: "us-east-1"}, + }, + wantErr: false, + }, + { + name: "invalid - missing siteId", + request: APICreateRackRequest{Name: "Rack-01", Manufacturer: "NVIDIA", SerialNumber: "SN-001"}, + wantErr: true, + }, + { + name: "invalid - non-UUID siteId", + request: APICreateRackRequest{SiteID: "not-a-uuid", Name: "Rack-01", Manufacturer: "NVIDIA", SerialNumber: "SN-001"}, + wantErr: true, + }, + { + name: "invalid - missing name", + request: APICreateRackRequest{SiteID: "550e8400-e29b-41d4-a716-446655440000", Manufacturer: "NVIDIA", SerialNumber: "SN-001"}, + wantErr: true, + }, + { + name: "invalid - missing manufacturer", + request: APICreateRackRequest{SiteID: "550e8400-e29b-41d4-a716-446655440000", Name: "Rack-01", SerialNumber: "SN-001"}, + wantErr: true, + }, + { + name: "invalid - missing serialNumber", + request: APICreateRackRequest{SiteID: "550e8400-e29b-41d4-a716-446655440000", Name: "Rack-01", Manufacturer: "NVIDIA"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.request.Validate() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAPICreateRackRequest_ToProtoRack(t *testing.T) { + model := "NVL72" + desc := "Test rack" + req := APICreateRackRequest{ + Name: "Rack-01", + Manufacturer: "NVIDIA", + SerialNumber: "SN-001", + Model: &model, + Description: &desc, + Location: &APIRackLocation{ + Region: "us-east-1", + Datacenter: "DC1", + Room: "A", + Position: "1", + }, + } + + proto := req.ToProtoRack() + assert.NotNil(t, proto) + assert.NotNil(t, proto.Info) + assert.Equal(t, "Rack-01", proto.Info.Name) + assert.Equal(t, "NVIDIA", proto.Info.Manufacturer) + assert.Equal(t, "SN-001", proto.Info.SerialNumber) + assert.Equal(t, &model, proto.Info.Model) + assert.Equal(t, &desc, proto.Info.Description) + assert.NotNil(t, proto.Location) + assert.Equal(t, "us-east-1", proto.Location.Region) + assert.Equal(t, "DC1", proto.Location.Datacenter) +} + +func TestAPIRackUpdateRequest_Validate(t *testing.T) { + tests := []struct { + name string + request APIRackUpdateRequest + wantErr bool + }{ + { + name: "valid - with siteId and name", + request: APIRackUpdateRequest{SiteID: "550e8400-e29b-41d4-a716-446655440000", Name: strPtr("Updated")}, + wantErr: false, + }, + { + name: "invalid - siteId only (no mutable fields)", + request: APIRackUpdateRequest{SiteID: "550e8400-e29b-41d4-a716-446655440000"}, + wantErr: true, + }, + { + name: "invalid - non-UUID siteId", + request: APIRackUpdateRequest{SiteID: "not-a-uuid", Name: strPtr("Updated")}, + wantErr: true, + }, + { + name: "invalid - missing siteId", + request: APIRackUpdateRequest{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.request.Validate() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAPIRackUpdateRequest_ToProtoRack(t *testing.T) { + name := "Updated-Rack" + manufacturer := "Dell" + region := "us-west-2" + req := APIRackUpdateRequest{ + SiteID: "550e8400-e29b-41d4-a716-446655440000", + Name: &name, + Manufacturer: &manufacturer, + Location: &APIRackUpdateLocation{ + Region: ®ion, + }, + } + + proto := req.ToProtoRack("test-rack-id") + assert.NotNil(t, proto) + assert.NotNil(t, proto.Info) + assert.Equal(t, "test-rack-id", proto.Info.Id.Id) + assert.Equal(t, "Updated-Rack", proto.Info.Name) + assert.Equal(t, "Dell", proto.Info.Manufacturer) + assert.NotNil(t, proto.Location) + assert.Equal(t, "us-west-2", proto.Location.Region) +} + +func TestNewAPICreateRackResponse(t *testing.T) { + tests := []struct { + name string + resp *rlav1.CreateExpectedRackResponse + expected *APICreateRackResponse + }{ + { + name: "nil response returns empty", + resp: nil, + expected: &APICreateRackResponse{}, + }, + { + name: "valid response", + resp: &rlav1.CreateExpectedRackResponse{ + Id: &rlav1.UUID{Id: "new-rack-id"}, + }, + expected: &APICreateRackResponse{ID: "new-rack-id"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NewAPICreateRackResponse(tt.resp) + assert.NotNil(t, result) + assert.Equal(t, tt.expected.ID, result.ID) + }) + } +} + +func TestNewAPIRackUpdateResponse(t *testing.T) { + tests := []struct { + name string + resp *rlav1.PatchRackResponse + expected *APIRackUpdateResponse + }{ + { + name: "nil response returns empty", + resp: nil, + expected: &APIRackUpdateResponse{}, + }, + { + name: "valid response", + resp: &rlav1.PatchRackResponse{ + Report: "Rack updated successfully", + }, + expected: &APIRackUpdateResponse{Report: "Rack updated successfully"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NewAPIRackUpdateResponse(tt.resp) + assert.NotNil(t, result) + assert.Equal(t, tt.expected.Report, result.Report) + }) + } +} + func TestAPIBringUpRackRequest_Validate(t *testing.T) { tests := []struct { name string diff --git a/api/pkg/api/routes.go b/api/pkg/api/routes.go index d721e22a..ffdb6889 100644 --- a/api/pkg/api/routes.go +++ b/api/pkg/api/routes.go @@ -820,6 +820,11 @@ func NewAPIRoutes(dbSession *cdb.Session, tc tClient.Client, tnc tClient.Namespa Handler: apiHandler.NewGetSkuHandler(dbSession, tc, cfg), }, // Rack endpoints (RLA) + { + Path: apiPathPrefix + "/rack", + Method: http.MethodPost, + Handler: apiHandler.NewCreateRackHandler(dbSession, tc, scp, cfg), + }, { Path: apiPathPrefix + "/rack", Method: http.MethodGet, @@ -850,6 +855,11 @@ func NewAPIRoutes(dbSession *cdb.Session, tc tClient.Client, tnc tClient.Namespa Method: http.MethodGet, Handler: apiHandler.NewGetRackHandler(dbSession, tc, scp, cfg), }, + { + Path: apiPathPrefix + "/rack/:id", + Method: http.MethodPatch, + Handler: apiHandler.NewUpdateRackHandler(dbSession, tc, scp, cfg), + }, { Path: apiPathPrefix + "/rack/:id/validation", Method: http.MethodGet, diff --git a/api/pkg/api/routes_test.go b/api/pkg/api/routes_test.go index e657552b..ab3ba277 100644 --- a/api/pkg/api/routes_test.go +++ b/api/pkg/api/routes_test.go @@ -80,7 +80,7 @@ func TestNewAPIRoutes(t *testing.T) { "machine-validation": 11, "dpu-extension-service": 7, "sku": 2, - "rack": 10, + "rack": 12, "tray": 8, "stats": 4, } diff --git a/docs/index.html b/docs/index.html index c8b203ef..b7d090b6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -437,7 +437,7 @@ -

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Rack

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack

Response samples

Content type
application/json
[
  • {
    }
]

Create a Rack

Create a new expected Rack definition.

+

Defines the expected configuration of a rack including its components, location, and device information. The rack will be tracked by RLA for validation and lifecycle management.

+

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

+
Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

+
Request Body schema: application/json
required
siteId
required
string <uuid>

ID of the Site

+
name
required
string

Name of the Rack

+
manufacturer
required
string

Manufacturer of the Rack

+
model
string

Model of the Rack

+
serialNumber
required
string

Serial number of the Rack

+
description
string

Description of the Rack

+
object (RackLocation)

Physical location of a Rack

+
Array of objects (RackComponent)

Components to include in the Rack

+

Responses

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "name": "Rack-01",
  • "manufacturer": "NVIDIA",
  • "serialNumber": "SN-RACK-001"
}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000"
}

Retrieve a Rack

Get a Rack by ID.

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

@@ -3153,7 +3187,43 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "name": "Rack-01",
  • "manufacturer": "Dell",
  • "model": "PowerEdge R750",
  • "serialNumber": "SN-RACK-001",
  • "description": "Primary compute rack",
  • "location": {
    },
  • "components": [
    ]
}

Validate Racks

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/{id}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "name": "Rack-01",
  • "manufacturer": "Dell",
  • "model": "PowerEdge R750",
  • "serialNumber": "SN-RACK-001",
  • "description": "Primary compute rack",
  • "location": {
    },
  • "components": [
    ]
}

Update a Rack

Patch an existing Rack's fields by ID.

+

Updates only the fields provided in the request body. Fields not included are left unchanged.

+

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

+
Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

+
id
required
string <uuid>

ID of the Rack

+
Request Body schema: application/json
required
siteId
required
string <uuid>

ID of the Site

+
name
string

Updated name of the Rack

+
manufacturer
string

Updated manufacturer of the Rack

+
model
string

Updated model of the Rack

+
serialNumber
string

Updated serial number of the Rack

+
description
string

Updated description of the Rack

+
object (RackLocation)

Physical location of a Rack

+

Responses

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "name": "Rack-01-Updated"
}

Response samples

Content type
application/json
{
  • "report": "Rack updated successfully"
}

Validate Racks

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "onlyInExpectedCount": 0,
  • "onlyInActualCount": 0,
  • "driftCount": 0,
  • "matchCount": 10
}

Validate a Rack

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/validation

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "onlyInExpectedCount": 0,
  • "onlyInActualCount": 0,
  • "driftCount": 0,
  • "matchCount": 10
}

Validate a Rack

Validate a Rack's components by comparing expected vs actual state.

@@ -3197,7 +3267,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "onlyInExpectedCount": 0,
  • "onlyInActualCount": 0,
  • "driftCount": 0,
  • "matchCount": 5
}

Power control Racks

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/{id}/validation

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "onlyInExpectedCount": 0,
  • "onlyInActualCount": 0,
  • "driftCount": 0,
  • "matchCount": 5
}

Power control Racks

Power control Racks with optional filters. If no filter is specified, targets all racks in the Site.

@@ -3219,7 +3289,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "off"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Power control a Rack

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/power

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "off"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Power control a Rack

Power control a Rack identified by Rack UUID.

@@ -3255,7 +3325,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update Racks

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/{id}/power

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update Racks

Update firmware on Racks with optional name filter. If no filter is specified, targets all racks in the Site.

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

@@ -3275,7 +3345,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update a Rack

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/firmware

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update a Rack

Update firmware on a Rack identified by Rack UUID.

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

@@ -3295,7 +3365,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "version": "24.11.0"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up Racks

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/{id}/firmware

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "version": "24.11.0"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up Racks

Bring up Racks with optional name filter. If no filter is specified, targets all racks in the Site.

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

@@ -3315,7 +3385,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up a Rack

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/bringup

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up a Rack

Bring up a Rack identified by Rack UUID.

Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.

@@ -3335,7 +3405,7 @@ " class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Tray

https://carbide-rest-api.carbide.svc.cluster.local/v2/org/{org}/carbide/rack/{id}/bringup

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Tray

Tray operations

Retrieve all Trays