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 @@
-[- {
- "id": "550e8400-e29b-41d4-a716-446655440000",
- "name": "Rack-01",
- "manufacturer": "Dell",
- "model": "PowerEdge R750",
- "serialNumber": "SN-RACK-001",
- "description": "Primary compute rack",
- "location": {
- "region": "us-east-1",
- "datacenter": "DC-01",
- "room": "Room-A",
- "position": "A1"
}, - "components": [
- {
- "id": "660e8400-e29b-41d4-a716-446655440001",
- "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
- "type": "ComponentTypeCompute",
- "name": "Server-01",
- "serialNumber": "SN-SRV-001",
- "manufacturer": "Dell",
- "firmwareVersion": "2.1.0",
- "slotId": 1,
- "trayIdx": 0,
- "hostId": 0,
- "bmcs": [
- {
- "type": "BmcTypeHost",
- "macAddress": "AA:BB:CC:DD:EE:01",
- "ipAddress": "10.0.0.101"
}
], - "powerState": "on"
}
]
}
][- {
- "id": "550e8400-e29b-41d4-a716-446655440000",
- "name": "Rack-01",
- "manufacturer": "Dell",
- "model": "PowerEdge R750",
- "serialNumber": "SN-RACK-001",
- "description": "Primary compute rack",
- "location": {
- "region": "us-east-1",
- "datacenter": "DC-01",
- "room": "Room-A",
- "position": "A1"
}, - "components": [
- {
- "id": "660e8400-e29b-41d4-a716-446655440001",
- "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
- "type": "ComponentTypeCompute",
- "name": "Server-01",
- "serialNumber": "SN-SRV-001",
- "manufacturer": "Dell",
- "firmwareVersion": "2.1.0",
- "slotId": 1,
- "trayIdx": 0,
- "hostId": 0,
- "bmcs": [
- {
- "type": "BmcTypeHost",
- "macAddress": "AA:BB:CC:DD:EE:01",
- "ipAddress": "10.0.0.101"
}
], - "powerState": "on"
}
]
}
]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.
| org required | string Name of the Org + |
| 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 + |
{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "name": "Rack-01",
- "manufacturer": "NVIDIA",
- "serialNumber": "SN-RACK-001"
}{- "id": "550e8400-e29b-41d4-a716-446655440000"
}{- "id": "550e8400-e29b-41d4-a716-446655440000",
- "name": "Rack-01",
- "manufacturer": "Dell",
- "model": "PowerEdge R750",
- "serialNumber": "SN-RACK-001",
- "description": "Primary compute rack",
- "location": {
- "region": "us-east-1",
- "datacenter": "DC-01",
- "room": "Room-A",
- "position": "A1"
}, - "components": [
- {
- "id": "660e8400-e29b-41d4-a716-446655440001",
- "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
- "type": "ComponentTypeCompute",
- "name": "Server-01",
- "serialNumber": "SN-SRV-001",
- "manufacturer": "Dell",
- "firmwareVersion": "2.1.0",
- "slotId": 1,
- "trayIdx": 0,
- "hostId": 0,
- "bmcs": [
- {
- "type": "BmcTypeHost",
- "macAddress": "AA:BB:CC:DD:EE:01",
- "ipAddress": "10.0.0.101"
}
], - "powerState": "on"
}
]
}{- "id": "550e8400-e29b-41d4-a716-446655440000",
- "name": "Rack-01",
- "manufacturer": "Dell",
- "model": "PowerEdge R750",
- "serialNumber": "SN-RACK-001",
- "description": "Primary compute rack",
- "location": {
- "region": "us-east-1",
- "datacenter": "DC-01",
- "room": "Room-A",
- "position": "A1"
}, - "components": [
- {
- "id": "660e8400-e29b-41d4-a716-446655440001",
- "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
- "type": "ComponentTypeCompute",
- "name": "Server-01",
- "serialNumber": "SN-SRV-001",
- "manufacturer": "Dell",
- "firmwareVersion": "2.1.0",
- "slotId": 1,
- "trayIdx": 0,
- "hostId": 0,
- "bmcs": [
- {
- "type": "BmcTypeHost",
- "macAddress": "AA:BB:CC:DD:EE:01",
- "ipAddress": "10.0.0.101"
}
], - "powerState": "on"
}
]
}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.
| org required | string Name of the Org + |
| id required | string <uuid> ID of the Rack + |
| 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 + |
{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "name": "Rack-01-Updated"
}{- "report": "Rack updated successfully"
}{- "diffs": [ ],
- "totalDiffs": 0,
- "onlyInExpectedCount": 0,
- "onlyInActualCount": 0,
- "driftCount": 0,
- "matchCount": 10
}{- "diffs": [ ],
- "totalDiffs": 0,
- "onlyInExpectedCount": 0,
- "onlyInActualCount": 0,
- "driftCount": 0,
- "matchCount": 10
}{- "diffs": [ ],
- "totalDiffs": 0,
- "onlyInExpectedCount": 0,
- "onlyInActualCount": 0,
- "driftCount": 0,
- "matchCount": 5
}{- "diffs": [ ],
- "totalDiffs": 0,
- "onlyInExpectedCount": 0,
- "onlyInActualCount": 0,
- "driftCount": 0,
- "matchCount": 5
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "state": "off"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "state": "off"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "state": "on"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "state": "on"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000"
]
}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.
Error response when user is not authorized to call an endpoint or retrieve/modify objects
{- "siteId": "550e8400-e29b-41d4-a716-446655440000"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
]
}Update firmware on a Rack identified by Rack UUID.
Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.
Error response when user is not authorized to call an endpoint or retrieve/modify objects
{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "version": "24.11.0"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000",
- "version": "24.11.0"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000"
]
}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.
Error response when user is not authorized to call an endpoint or retrieve/modify objects
{- "siteId": "550e8400-e29b-41d4-a716-446655440000"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000",
- "660e8400-e29b-41d4-a716-446655440001"
]
}Bring up a Rack identified by Rack UUID.
Org must have an Infrastructure Provider entity. User must have FORGE_PROVIDER_ADMIN authorization role.
Error response when user is not authorized to call an endpoint or retrieve/modify objects
{- "siteId": "550e8400-e29b-41d4-a716-446655440000"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000"
]
}{- "siteId": "550e8400-e29b-41d4-a716-446655440000"
}{- "taskIds": [
- "550e8400-e29b-41d4-a716-446655440000"
]
}