diff --git a/rest-api/api/pkg/api/handler/instance.go b/rest-api/api/pkg/api/handler/instance.go index 55cfc96a8b..5a06f18097 100644 --- a/rest-api/api/pkg/api/handler/instance.go +++ b/rest-api/api/pkg/api/handler/instance.go @@ -89,6 +89,45 @@ func NewCreateInstanceHandler(dbSession *cdb.Session, tc temporalClient.Client, } } +// validateTemplatedIpxeOsForSite guards the Templated iPXE Operating System +// selection paths (Instance create / update / batch-create) before the OS ID is +// sent to Core. Caller authorization and tenant/OS ownership are already enforced +// by the handlers (ValidateOrgMembership / ValidateUserRoles) and the per-request +// ownership check, so this enforces the site-availability contract specific to +// templated OSes: the OS definition must be synchronized to the Instance's Site +// (a Synced OperatingSystemSiteAssociation) so the Site can render the template +// at provisioning time. +// +// This is intentionally independent of how the OS was created or which side owns +// its definition: single-site OSes sync bidirectionally with nico-core, while +// multi-site OSes are REST-owned and pushed out to their Sites. In every case a +// Synced association is the signal that the definition is actually available at +// the Site, so we gate on that alone. +func validateTemplatedIpxeOsForSite(ctx context.Context, dbSession *cdb.Session, logger *zerolog.Logger, os *cdbm.OperatingSystem, siteID uuid.UUID) *cutil.APIError { + ossaDAO := cdbm.NewOperatingSystemSiteAssociationDAO(dbSession) + _, ossaCount, err := ossaDAO.GetAll( + ctx, + nil, + cdbm.OperatingSystemSiteAssociationFilterInput{ + OperatingSystemIDs: []uuid.UUID{os.ID}, + SiteIDs: []uuid.UUID{siteID}, + Statuses: []string{cdbm.OperatingSystemSiteAssociationStatusSynced}, + }, + cdbp.PageInput{Limit: cutil.GetPtr(1)}, + nil, + ) + if err != nil { + logger.Error().Err(err).Str("operatingSystemId", os.ID.String()).Msg("error retrieving OperatingSystemSiteAssociations for Templated iPXE OS") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to retrieve OperatingSystem site associations, DB error", nil) + } + if ossaCount == 0 { + logger.Warn().Str("operatingSystemId", os.ID.String()).Str("siteId", siteID.String()).Msg("Templated iPXE Operating System is not synchronized to the Instance's Site") + return cutil.NewAPIError(http.StatusBadRequest, "Templated iPXE Operating System specified in request is not synchronized to the Instance's Site", nil) + } + + return nil +} + // Returns either a default OS or an existing instance OS config. // apiRequest will be mutated for use in create. // osConfig will hold the struct/data for use with Temporal/NICo calls. @@ -213,6 +252,20 @@ func (cih CreateInstanceHandler) buildInstanceCreateRequestOsConfig(c echo.Conte }, UserData: apiRequest.UserData, }, osID, nil + } else if os.Type == cdbm.OperatingSystemTypeTemplatedIPXE { + if apiErr := validateTemplatedIpxeOsForSite(ctx, cih.dbSession, logger, os, site.ID); apiErr != nil { + return nil, nil, apiErr + } + return &corev1.InstanceOperatingSystemConfig{ + RunProvisioningInstructionsOnEveryBoot: *apiRequest.AlwaysBootWithCustomIpxe, + PhoneHomeEnabled: *apiRequest.PhoneHomeEnabled, + Variant: &corev1.InstanceOperatingSystemConfig_OperatingSystemId{ + OperatingSystemId: &corev1.OperatingSystemId{ + Value: os.ID.String(), + }, + }, + UserData: apiRequest.UserData, + }, osID, nil } else { return &corev1.InstanceOperatingSystemConfig{ PhoneHomeEnabled: *apiRequest.PhoneHomeEnabled, @@ -2098,6 +2151,20 @@ func (uih UpdateInstanceHandler) buildInstanceUpdateRequestOsConfig(c echo.Conte }, UserData: userData, }, osID, nil + } else if os.Type == cdbm.OperatingSystemTypeTemplatedIPXE { + if apiErr := validateTemplatedIpxeOsForSite(ctx, uih.dbSession, logger, os, site.ID); apiErr != nil { + return nil, nil, apiErr + } + return &corev1.InstanceOperatingSystemConfig{ + RunProvisioningInstructionsOnEveryBoot: alwaysBootWithCustomIpxe, + PhoneHomeEnabled: phoneHomeEnabled, + Variant: &corev1.InstanceOperatingSystemConfig_OperatingSystemId{ + OperatingSystemId: &corev1.OperatingSystemId{ + Value: os.ID.String(), + }, + }, + UserData: userData, + }, osID, nil } else if os.Type == cdbm.OperatingSystemTypeImage { return &corev1.InstanceOperatingSystemConfig{ PhoneHomeEnabled: phoneHomeEnabled, diff --git a/rest-api/api/pkg/api/handler/instance_test.go b/rest-api/api/pkg/api/handler/instance_test.go index 4269c0c515..05b60f4efe 100644 --- a/rest-api/api/pkg/api/handler/instance_test.go +++ b/rest-api/api/pkg/api/handler/instance_test.go @@ -35,6 +35,7 @@ import ( swe "github.com/NVIDIA/infra-controller/rest-api/site-workflow/pkg/error" "github.com/google/uuid" "github.com/labstack/echo/v4" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -10292,3 +10293,187 @@ func TestInstanceHandler_GetStatusDetails(t *testing.T) { }) } } + +// newTemplatedOsEchoContext returns a bare echo.Context whose request carries a +// background context, which is all the buildInstance*OsConfig helpers require. +func newTemplatedOsEchoContext(t *testing.T) echo.Context { + t.Helper() + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", nil) + req = req.WithContext(context.Background()) + return e.NewContext(req, httptest.NewRecorder()) +} + +// assertTemplatedOsConfig asserts that a built InstanceOperatingSystemConfig +// references the Operating System by ID (the Templated iPXE variant) rather than +// carrying an inline iPXE script or an OS image reference. +func assertTemplatedOsConfig(t *testing.T, osConfig *corev1.InstanceOperatingSystemConfig, osID uuid.UUID) { + t.Helper() + require.NotNil(t, osConfig) + variant, ok := osConfig.Variant.(*corev1.InstanceOperatingSystemConfig_OperatingSystemId) + require.Truef(t, ok, "expected OperatingSystemId variant for Templated iPXE OS, got %T", osConfig.Variant) + require.NotNil(t, variant.OperatingSystemId) + assert.Equal(t, osID.String(), variant.OperatingSystemId.Value) +} + +// TestBuildInstanceOsConfig_TemplatedIPXE verifies that a Templated iPXE +// Operating System is translated into an InstanceOperatingSystemConfig that +// references the OS by ID (corev1.InstanceOperatingSystemConfig_OperatingSystemId) +// across the create, update and batch-create instance paths, and that the shared +// templated-OS site validator rejects OSes that are not usable at the Site. +// +// Unlike raw iPXE (inline script) and Image (OS image ID) types, a Templated +// iPXE OS is rendered on the Site from its template, so only the OS ID is +// propagated to Core, and only once the definition is synchronized to that Site. +func TestBuildInstanceOsConfig_TemplatedIPXE(t *testing.T) { + dbSession := testMachineInitDB(t) + defer dbSession.Close() + + common.TestSetupSchema(t, dbSession) + + cfg := common.GetTestConfig() + logger := zerolog.Nop() + + org := "tmpl-os-instance-org" + user := testInstanceBuildUser(t, dbSession, uuid.NewString(), org, []string{"tenant_admin"}) + tenant := testInstanceBuildTenant(t, dbSession, "tmpl-os-instance-tenant", org, user) + + ip := testMachineBuildInfrastructureProvider(t, dbSession, "tmpl-os-instance-ip-org", "tmpl-os-instance-provider") + site := testMachineBuildSite(t, dbSession, ip, "tmpl-os-instance-site", cdbm.SiteStatusRegistered) + + // buildOS builds a tenant-owned Templated iPXE OS. + buildOS := func(name string) *cdbm.OperatingSystem { + return testInstanceBuildOperatingSystem(t, dbSession, name, tenant, + cdbm.OperatingSystemTypeTemplatedIPXE, false, nil, false, cdbm.OperatingSystemStatusReady, user) + } + + // An OS synchronized (Synced association) to the Site: the happy path. + osSynced := buildOS("tmpl-os-instance-os-synced") + testInstanceBuildOperatingSystemSiteAssociation(t, dbSession, site.ID, osSynced.ID) + + t.Run("create", func(t *testing.T) { + ec := newTemplatedOsEchoContext(t) + h := CreateInstanceHandler{dbSession: dbSession, cfg: cfg} + apiReq := &model.APIInstanceCreateRequest{ + TenantID: tenant.ID.String(), + OperatingSystemID: cutil.GetPtr(osSynced.ID.String()), + } + + osConfig, osID, apiErr := h.buildInstanceCreateRequestOsConfig(ec, &logger, apiReq, site) + require.Nil(t, apiErr) + require.NotNil(t, osID) + assert.Equal(t, osSynced.ID, *osID) + assertTemplatedOsConfig(t, osConfig, osSynced.ID) + }) + + t.Run("update", func(t *testing.T) { + ec := newTemplatedOsEchoContext(t) + h := UpdateInstanceHandler{dbSession: dbSession, cfg: cfg} + // The instance is passed directly (not loaded from the DB) and only needs + // its Tenant populated for the ownership check. + instance := &cdbm.Instance{ + ID: uuid.New(), + TenantID: tenant.ID, + Tenant: tenant, + } + apiReq := &model.APIInstanceUpdateRequest{ + OperatingSystemID: cutil.GetPtr(osSynced.ID.String()), + } + + osConfig, osID, apiErr := h.buildInstanceUpdateRequestOsConfig(ec, &logger, apiReq, instance, site) + require.Nil(t, apiErr) + require.NotNil(t, osID) + assert.Equal(t, osSynced.ID, *osID) + assertTemplatedOsConfig(t, osConfig, osSynced.ID) + }) + + t.Run("batch create", func(t *testing.T) { + ec := newTemplatedOsEchoContext(t) + h := BatchCreateInstanceHandler{dbSession: dbSession, cfg: cfg} + apiReq := &model.APIBatchInstanceCreateRequest{ + TenantID: tenant.ID.String(), + OperatingSystemID: cutil.GetPtr(osSynced.ID.String()), + } + + osConfig, osID, apiErr := h.buildBatchInstanceCreateRequestOsConfig(ec, &logger, apiReq, site) + require.Nil(t, apiErr) + require.NotNil(t, osID) + assert.Equal(t, osSynced.ID, *osID) + assertTemplatedOsConfig(t, osConfig, osSynced.ID) + }) + + // The following cases exercise the shared validator through the create path; + // the same validator gates the update and batch paths. + + // Selection is gated on site availability, not on ownership: any Templated iPXE + // OS that is present (Synced association) at its Site is a usable definition. + t.Run("allows any templated OS synchronized to Site", func(t *testing.T) { + osOther := buildOS("tmpl-os-instance-os-other") + testInstanceBuildOperatingSystemSiteAssociation(t, dbSession, site.ID, osOther.ID) + + ec := newTemplatedOsEchoContext(t) + h := CreateInstanceHandler{dbSession: dbSession, cfg: cfg} + apiReq := &model.APIInstanceCreateRequest{ + TenantID: tenant.ID.String(), + OperatingSystemID: cutil.GetPtr(osOther.ID.String()), + } + + osConfig, osID, apiErr := h.buildInstanceCreateRequestOsConfig(ec, &logger, apiReq, site) + require.Nil(t, apiErr) + require.NotNil(t, osID) + assert.Equal(t, osOther.ID, *osID) + assertTemplatedOsConfig(t, osConfig, osOther.ID) + }) + + // Only a Synced association marks a definition as available at the Site. An + // OS with no association, or one whose association is still Syncing or has + // Errored, must be rejected with BadRequest and no osConfig/osID. + rejectCases := []struct { + name string + status *string // nil => no association at all + }{ + {name: "rejects templated OS not synchronized to Site", status: nil}, + {name: "rejects templated OS with Syncing association", status: cutil.GetPtr(cdbm.OperatingSystemSiteAssociationStatusSyncing)}, + {name: "rejects templated OS with Error association", status: cutil.GetPtr(cdbm.OperatingSystemSiteAssociationStatusError)}, + } + for i, tc := range rejectCases { + t.Run(tc.name, func(t *testing.T) { + os := buildOS(fmt.Sprintf("tmpl-os-instance-os-reject-%d", i)) + if tc.status != nil { + buildOperatingSystemSiteAssociationWithStatus(t, dbSession, site.ID, os.ID, *tc.status) + } + + ec := newTemplatedOsEchoContext(t) + h := CreateInstanceHandler{dbSession: dbSession, cfg: cfg} + apiReq := &model.APIInstanceCreateRequest{ + TenantID: tenant.ID.String(), + OperatingSystemID: cutil.GetPtr(os.ID.String()), + } + + osConfig, osID, apiErr := h.buildInstanceCreateRequestOsConfig(ec, &logger, apiReq, site) + require.NotNil(t, apiErr) + assert.Equal(t, http.StatusBadRequest, apiErr.Code) + assert.Nil(t, osConfig) + assert.Nil(t, osID) + }) + } +} + +// buildOperatingSystemSiteAssociationWithStatus inserts an +// OperatingSystemSiteAssociation with an explicit status, so tests can exercise +// non-Synced states (Syncing / Error) that the templated-OS site validator must +// reject. +func buildOperatingSystemSiteAssociationWithStatus(t *testing.T, dbSession *cdb.Session, siteID, osID uuid.UUID, status string) { + t.Helper() + ossa := &cdbm.OperatingSystemSiteAssociation{ + ID: uuid.New(), + OperatingSystemID: osID, + SiteID: siteID, + Version: cutil.GetPtr("1234"), + Status: status, + Created: cdb.GetCurTime(), + Updated: cdb.GetCurTime(), + } + _, err := dbSession.DB.NewInsert().Model(ossa).Exec(context.Background()) + require.NoError(t, err) +} diff --git a/rest-api/api/pkg/api/handler/instancebatch.go b/rest-api/api/pkg/api/handler/instancebatch.go index 6b693b26b5..732f1beac1 100644 --- a/rest-api/api/pkg/api/handler/instancebatch.go +++ b/rest-api/api/pkg/api/handler/instancebatch.go @@ -186,6 +186,20 @@ func (bcih BatchCreateInstanceHandler) buildBatchInstanceCreateRequestOsConfig(c }, UserData: apiRequest.UserData, }, osID, nil + } else if os.Type == cdbm.OperatingSystemTypeTemplatedIPXE { + if apiErr := validateTemplatedIpxeOsForSite(ctx, bcih.dbSession, logger, os, site.ID); apiErr != nil { + return nil, nil, apiErr + } + return &corev1.InstanceOperatingSystemConfig{ + RunProvisioningInstructionsOnEveryBoot: *apiRequest.AlwaysBootWithCustomIpxe, + PhoneHomeEnabled: *apiRequest.PhoneHomeEnabled, + Variant: &corev1.InstanceOperatingSystemConfig_OperatingSystemId{ + OperatingSystemId: &corev1.OperatingSystemId{ + Value: os.ID.String(), + }, + }, + UserData: apiRequest.UserData, + }, osID, nil } else { return &corev1.InstanceOperatingSystemConfig{ PhoneHomeEnabled: *apiRequest.PhoneHomeEnabled, diff --git a/rest-api/api/pkg/api/handler/ipxetemplate.go b/rest-api/api/pkg/api/handler/ipxetemplate.go new file mode 100644 index 0000000000..1985eaf38f --- /dev/null +++ b/rest-api/api/pkg/api/handler/ipxetemplate.go @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package handler + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + tclient "go.temporal.io/sdk/client" + + "github.com/NVIDIA/infra-controller/rest-api/api/internal/config" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/handler/util/common" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/model" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/pagination" + cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" + cdb "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db" + cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" + cdbp "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/paginator" +) + +// ~~~~~ GetAll Handler ~~~~~ // + +// GetAllIpxeTemplateHandler is the API Handler for getting all iPXE templates +type GetAllIpxeTemplateHandler struct { + dbSession *cdb.Session + tc tclient.Client + cfg *config.Config + tracerSpan *cutil.TracerSpan +} + +// NewGetAllIpxeTemplateHandler initializes and returns a new handler for getting all iPXE templates +func NewGetAllIpxeTemplateHandler(dbSession *cdb.Session, tc tclient.Client, cfg *config.Config) GetAllIpxeTemplateHandler { + return GetAllIpxeTemplateHandler{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + tracerSpan: cutil.NewTracerSpan(), + } +} + +// Handle godoc +// @Summary Get all iPXE templates +// @Description Get all iPXE templates propagated from nico-core. Templates are global (one row per stable core template UUID); per-site availability is recorded internally. The `siteId` query parameter is optional and may be repeated to restrict results to templates available at one or more sites. When omitted, a Provider Admin/Viewer receives templates available at any site owned by their infrastructure provider; a Tenant Admin receives templates available at any site whose provider the tenant has a Tenant Account on. +// @Tags iPXE Template +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param org path string true "Name of NGC organization" +// @Param siteId query []string false "Optional site ID(s); may be repeated to restrict results to templates available at any of the sites" +// @Param pageNumber query integer false "Page number of results returned" +// @Param pageSize query integer false "Number of results per page" +// @Param orderBy query string false "Order by field" +// @Success 200 {object} []model.APIIpxeTemplate +// @Router /v2/org/{org}/nico/ipxe-template [get] +func (h GetAllIpxeTemplateHandler) Handle(c echo.Context) error { + org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("IpxeTemplate", "GetAll", c, h.tracerSpan) + if handlerSpan != nil { + defer handlerSpan.End() + } + + 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 role (Provider Admin/Viewer or Tenant Admin) and org membership + infrastructureProvider, tenant, apiError := common.IsProviderOrTenant(ctx, logger, h.dbSession, org, dbUser, true, false) + if apiError != nil { + return cutil.NewAPIErrorResponse(c, apiError.Code, apiError.Message, apiError.Data) + } + + // Parse optional siteId query parameters. Multiple values (repeated + // `?siteId=...&siteId=...`) are supported. + if err := common.ValidateKnownQueryParams(c.QueryParams(), model.APIIpxeTemplateGetAllRequest{}, pagination.PageRequest{}); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, err.Error(), nil) + } + + requestedSiteIDStrs := c.QueryParams()["siteId"] + requestedSiteIDs := make([]uuid.UUID, 0, len(requestedSiteIDStrs)) + for _, s := range requestedSiteIDStrs { + if s == "" { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid siteId in query parameter: empty value", nil) + } + parsed, perr := uuid.Parse(s) + if perr != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid siteId in query parameter: %s", s), nil) + } + requestedSiteIDs = append(requestedSiteIDs, parsed) + } + + // Build the caller's authorized site set, tracking which sites come from the + // provider path vs the tenant path. A site can be in both sets for a + // dual-role caller — provider access wins (fewer restrictions). + providerSites := mapset.NewSet[uuid.UUID]() + tenantSites := mapset.NewSet[uuid.UUID]() + + if infrastructureProvider != nil { + siteDAO := cdbm.NewSiteDAO(h.dbSession) + sites, _, serr := siteDAO.GetAll(ctx, nil, + cdbm.SiteFilterInput{InfrastructureProviderIDs: []uuid.UUID{infrastructureProvider.ID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if serr != nil { + logger.Error().Err(serr).Msg("error retrieving provider sites from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve provider sites, DB error", nil) + } + for i := range sites { + providerSites.Add(sites[i].ID) + } + } + + if tenant != nil { + tsDAO := cdbm.NewTenantSiteDAO(h.dbSession) + tss, _, terr := tsDAO.GetAll(ctx, nil, + cdbm.TenantSiteFilterInput{TenantIDs: []uuid.UUID{tenant.ID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if terr != nil { + logger.Error().Err(terr).Msg("error retrieving Tenant Site associations from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Tenant Site associations, DB error", nil) + } + for i := range tss { + tenantSites.Add(tss[i].SiteID) + } + } + + isAuthorized := func(id uuid.UUID) bool { + return providerSites.Contains(id) || tenantSites.Contains(id) + } + + // Determine the effective site filter: + // - siteId(s) provided: must all be authorized; use them as-is. + // - siteId(s) omitted: use the union of provider and tenant accessible sites. + var effectiveSiteIDs []uuid.UUID + if len(requestedSiteIDs) > 0 { + for _, id := range requestedSiteIDs { + if !isAuthorized(id) { + logger.Warn().Str("siteID", id.String()).Msg("org not authorized to access requested Site") + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, fmt.Sprintf("Current org is not authorized to access Site: %s", id.String()), nil) + } + } + effectiveSiteIDs = requestedSiteIDs + } else { + effectiveSiteIDs = providerSites.Union(tenantSites).ToSlice() + } + + // No authorized sites — neither provider-owned nor reachable via a tenant account. + if len(effectiveSiteIDs) == 0 { + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Current org is not associated with any Site", nil) + } + + // Validate pagination request + pageRequest := pagination.PageRequest{} + if err := c.Bind(&pageRequest); err != nil { + logger.Warn().Err(err).Msg("error binding pagination request data into API model") + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to parse request pagination data", nil) + } + if err := pageRequest.Validate(cdbm.IpxeTemplateOrderByFields); err != nil { + logger.Warn().Err(err).Msg("error validating pagination request data") + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Failed to validate pagination request data", err) + } + + // Resolve which template IDs are available at the authorized sites via + // the IpxeTemplateSiteAssociation table. + itsaDAO := cdbm.NewIpxeTemplateSiteAssociationDAO(h.dbSession) + associations, _, err := itsaDAO.GetAll(ctx, nil, + cdbm.IpxeTemplateSiteAssociationFilterInput{SiteIDs: effectiveSiteIDs}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if err != nil { + logger.Error().Err(err).Msg("error retrieving iPXE template site associations from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve iPXE template site associations, DB error", nil) + } + + templateIDSet := mapset.NewSet[uuid.UUID]() + for _, a := range associations { + templateIDSet.Add(a.IpxeTemplateID) + } + templateIDs := templateIDSet.ToSlice() + + templateDAO := cdbm.NewIpxeTemplateDAO(h.dbSession) + templates, total, err := templateDAO.GetAll( + ctx, + nil, + cdbm.IpxeTemplateFilterInput{IpxeTemplateIDs: templateIDs}, + cdbp.PageInput{ + Offset: pageRequest.Offset, + Limit: pageRequest.Limit, + OrderBy: pageRequest.OrderBy, + }, + ) + if err != nil { + logger.Error().Err(err).Msg("error retrieving iPXE templates from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve iPXE templates, DB error", nil) + } + + apiTemplates := []*model.APIIpxeTemplate{} + for i := range templates { + apiTemplates = append(apiTemplates, model.NewAPIIpxeTemplate(&templates[i])) + } + + pageResponse := pagination.NewPageResponse(*pageRequest.PageNumber, *pageRequest.PageSize, total, pageRequest.OrderByStr) + pageHeader, err := json.Marshal(pageResponse) + if err != nil { + logger.Error().Err(err).Msg("error marshaling pagination response") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to generate pagination response header", nil) + } + c.Response().Header().Set(pagination.ResponseHeaderName, string(pageHeader)) + + logger.Info().Msg("finishing API handler") + return c.JSON(http.StatusOK, apiTemplates) +} + +// ~~~~~ Get Handler ~~~~~ // + +// GetIpxeTemplateHandler is the API Handler for retrieving a single iPXE template +type GetIpxeTemplateHandler struct { + dbSession *cdb.Session + tc tclient.Client + cfg *config.Config + tracerSpan *cutil.TracerSpan +} + +// NewGetIpxeTemplateHandler initializes and returns a new handler to retrieve an iPXE template +func NewGetIpxeTemplateHandler(dbSession *cdb.Session, tc tclient.Client, cfg *config.Config) GetIpxeTemplateHandler { + return GetIpxeTemplateHandler{ + dbSession: dbSession, + tc: tc, + cfg: cfg, + tracerSpan: cutil.NewTracerSpan(), + } +} + +// Handle godoc +// @Summary Retrieve an iPXE template +// @Description Retrieve an iPXE template by its stable core ID. The caller must be authorized for at least one Site at which the template is currently available (Provider Admin/Viewer for a Site owned by their infrastructure provider, or Tenant Admin with a Tenant Account on a Site reporting the template). +// @Tags iPXE Template +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param org path string true "Name of NGC organization" +// @Param id path string true "Stable template ID (UUID from core)" +// @Success 200 {object} model.APIIpxeTemplate +// @Router /v2/org/{org}/nico/ipxe-template/{id} [get] +func (h GetIpxeTemplateHandler) Handle(c echo.Context) error { + org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("IpxeTemplate", "Get", c, h.tracerSpan) + if handlerSpan != nil { + defer handlerSpan.End() + } + + 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 role (Provider Admin/Viewer or Tenant Admin) — this also validates + // org membership, so no separate membership check is needed here. + infrastructureProvider, tenant, apiError := common.IsProviderOrTenant(ctx, logger, h.dbSession, org, dbUser, true, false) + if apiError != nil { + return cutil.NewAPIErrorResponse(c, apiError.Code, apiError.Message, apiError.Data) + } + + // Parse template ID from URL (this is the stable core template UUID, which is + // also the primary key in REST). + templateIDStr := c.Param("id") + templateID, err := uuid.Parse(templateIDStr) + if err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Invalid iPXE template ID: %s", templateIDStr), nil) + } + + logger = logger.With().Str("IpxeTemplate ID", templateIDStr).Logger() + h.tracerSpan.SetAttribute(handlerSpan, attribute.String("ipxe_template_id", templateIDStr), logger) + + templateDAO := cdbm.NewIpxeTemplateDAO(h.dbSession) + tmpl, err := templateDAO.Get(ctx, nil, templateID) + if err != nil { + if errors.Is(err, cdb.ErrDoesNotExist) { + return cutil.NewAPIErrorResponse(c, http.StatusNotFound, fmt.Sprintf("Could not find iPXE template with ID: %s", templateIDStr), nil) + } + logger.Error().Err(err).Msg("error retrieving iPXE template from DB") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve iPXE template, DB error", nil) + } + + // Authorization: caller must be associated (via provider ownership or tenant + // account) with at least one Site at which this template is currently + // reported. + itsaDAO := cdbm.NewIpxeTemplateSiteAssociationDAO(h.dbSession) + associations, _, err := itsaDAO.GetAll(ctx, nil, + cdbm.IpxeTemplateSiteAssociationFilterInput{IpxeTemplateIDs: []uuid.UUID{templateID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if err != nil { + logger.Error().Err(err).Msg("error retrieving iPXE template site associations") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to verify iPXE template authorization, DB error", nil) + } + + hasAccess, err := callerHasAccessToAnyAssociatedSite(ctx, logger, h.dbSession, infrastructureProvider, tenant, associations) + if err != nil { + logger.Error().Err(err).Msg("error verifying iPXE template site authorization") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to verify iPXE template authorization, DB error", nil) + } + if !hasAccess { + logger.Warn().Msg("caller is not authorized to access any Site associated with this iPXE template") + return cutil.NewAPIErrorResponse(c, http.StatusForbidden, "Current org is not authorized to access this iPXE template", nil) + } + + logger.Info().Msg("finishing API handler") + return c.JSON(http.StatusOK, model.NewAPIIpxeTemplate(tmpl)) +} + +// callerHasAccessToAnyAssociatedSite reports whether the caller (provider or +// tenant) has access to at least one site in the given association set. A non-nil +// error indicates a lookup failure and must be surfaced by callers as HTTP 500, +// rather than being collapsed into a "no access" (HTTP 403) answer. +func callerHasAccessToAnyAssociatedSite( + ctx context.Context, + logger zerolog.Logger, + dbSession *cdb.Session, + provider *cdbm.InfrastructureProvider, + tenant *cdbm.Tenant, + associations []cdbm.IpxeTemplateSiteAssociation, +) (bool, error) { + if len(associations) == 0 { + return false, nil + } + + siteIDs := make([]uuid.UUID, 0, len(associations)) + for _, a := range associations { + siteIDs = append(siteIDs, a.SiteID) + } + + // Provider path: any site owned by the caller's provider. + if provider != nil { + siteDAO := cdbm.NewSiteDAO(dbSession) + sites, _, serr := siteDAO.GetAll(ctx, nil, cdbm.SiteFilterInput{ + InfrastructureProviderIDs: []uuid.UUID{provider.ID}, + SiteIDs: siteIDs, + }, cdbp.PageInput{Limit: cutil.GetPtr(1)}, nil) + if serr != nil { + logger.Error().Err(serr).Msg("error retrieving provider sites for iPXE template authorization") + return false, serr + } + if len(sites) > 0 { + return true, nil + } + } + + // Tenant path: any site reachable via a TenantSite association. + if tenant != nil { + tsDAO := cdbm.NewTenantSiteDAO(dbSession) + tss, _, terr := tsDAO.GetAll(ctx, nil, cdbm.TenantSiteFilterInput{ + TenantIDs: []uuid.UUID{tenant.ID}, + SiteIDs: siteIDs, + }, cdbp.PageInput{Limit: cutil.GetPtr(1)}, nil) + if terr != nil { + logger.Error().Err(terr).Msg("error retrieving Tenant Site associations for iPXE template authorization") + return false, terr + } + if len(tss) > 0 { + return true, nil + } + } + + return false, nil +} diff --git a/rest-api/api/pkg/api/handler/ipxetemplate_test.go b/rest-api/api/pkg/api/handler/ipxetemplate_test.go new file mode 100644 index 0000000000..76424a6a64 --- /dev/null +++ b/rest-api/api/pkg/api/handler/ipxetemplate_test.go @@ -0,0 +1,365 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package handler + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/handler/util/common" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/model" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/pagination" + authz "github.com/NVIDIA/infra-controller/rest-api/auth/pkg/authorization" + "github.com/NVIDIA/infra-controller/rest-api/common/pkg/otelecho" + cdb "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db" + cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" + cdbp "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/paginator" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tmocks "go.temporal.io/sdk/mocks" +) + +// ipxeTemplateTestFixture wires up a provider, one of its sites, a tenant with +// access to that site, and two templates: one available at the site (via an +// IpxeTemplateSiteAssociation) and one that exists but is not available anywhere. +type ipxeTemplateTestFixture struct { + dbSession *cdb.Session + ipOrg string + tnOrg string + emptyIPOrg string + provider *cdbm.InfrastructureProvider + site *cdbm.Site + tenant *cdbm.Tenant + availTmpl *cdbm.IpxeTemplate + orphanTmpl *cdbm.IpxeTemplate + provUser *cdbm.User + tnUser *cdbm.User +} + +func buildIpxeTemplateFixture(t *testing.T) (*ipxeTemplateTestFixture, func()) { + ctx := context.Background() + dbSession := testMachineInitDB(t) + common.TestSetupSchema(t, dbSession) + + f := &ipxeTemplateTestFixture{ + ipOrg: "ipxet-ip-org", + tnOrg: "ipxet-tn-org", + emptyIPOrg: "ipxet-empty-ip-org", + } + + // A provider admin whose org owns the site, plus an empty provider org to + // exercise the "no associated site" path. + f.provUser = testMachineBuildUser(t, dbSession, uuid.NewString(), []string{f.ipOrg, f.emptyIPOrg}, []string{authz.ProviderAdminRole}) + f.provider = testMachineBuildInfrastructureProvider(t, dbSession, f.ipOrg, "ipxet-provider") + testMachineBuildInfrastructureProvider(t, dbSession, f.emptyIPOrg, "ipxet-empty-provider") + f.site = testMachineBuildSite(t, dbSession, f.provider, "ipxet-site", cdbm.SiteStatusRegistered) + + // A tenant admin with a tenant account on the provider's site. + f.tnUser = testMachineBuildUser(t, dbSession, uuid.NewString(), []string{f.tnOrg}, []string{authz.TenantAdminRole}) + f.tenant = testMachineBuildTenant(t, dbSession, f.tnOrg, "ipxet-tenant") + testBuildTenantSiteAssociation(t, dbSession, f.tnOrg, f.tenant.ID, f.site.ID, f.tnUser.ID) + + templateDAO := cdbm.NewIpxeTemplateDAO(dbSession) + availTmpl, err := templateDAO.Create(ctx, nil, cdbm.IpxeTemplateCreateInput{ + ID: uuid.New(), + Name: "ipxet-available", + Template: "#!ipxe\n", + Visibility: "Public", + }) + require.NoError(t, err) + f.availTmpl = availTmpl + + orphanTmpl, err := templateDAO.Create(ctx, nil, cdbm.IpxeTemplateCreateInput{ + ID: uuid.New(), + Name: "ipxet-orphan", + Template: "#!ipxe\n", + Visibility: "Public", + }) + require.NoError(t, err) + f.orphanTmpl = orphanTmpl + + itsaDAO := cdbm.NewIpxeTemplateSiteAssociationDAO(dbSession) + _, err = itsaDAO.Create(ctx, nil, cdbm.IpxeTemplateSiteAssociationCreateInput{IpxeTemplateID: availTmpl.ID, SiteID: f.site.ID}) + require.NoError(t, err) + + cleanup := func() { dbSession.Close() } + // Store the session on the fixture indirectly via closures used by the tests. + f.dbSession = dbSession + return f, cleanup +} + +func TestIpxeTemplateHandler_GetAll(t *testing.T) { + f, cleanup := buildIpxeTemplateFixture(t) + defer cleanup() + + ctx := context.Background() + cfg := common.GetTestConfig() + tc := &tmocks.Client{} + tracer, _, ctx := common.TestCommonTraceProviderSetup(t, ctx) + + tests := []struct { + name string + reqOrgName string + user *cdbm.User + siteID *uuid.UUID + queryParams url.Values + expectedErr bool + expectedStatus int + expectedNames []string + expectedPageNumber int + expectedPageSize int + expectedTotal int + }{ + { + name: "error when user not in request context", + reqOrgName: f.ipOrg, + user: nil, + expectedErr: true, + expectedStatus: http.StatusInternalServerError, + }, + { + name: "error when user not a member of org", + reqOrgName: "SomeOtherOrg", + user: f.provUser, + expectedErr: true, + expectedStatus: http.StatusForbidden, + }, + { + name: "provider admin sees templates available at owned sites", + reqOrgName: f.ipOrg, + user: f.provUser, + expectedErr: false, + expectedStatus: http.StatusOK, + expectedNames: []string{f.availTmpl.Name}, + expectedPageNumber: 1, + expectedPageSize: cdbp.DefaultLimit, + expectedTotal: 1, + }, + { + name: "tenant admin sees templates available at accessible sites", + reqOrgName: f.tnOrg, + user: f.tnUser, + expectedErr: false, + expectedStatus: http.StatusOK, + expectedNames: []string{f.availTmpl.Name}, + expectedPageNumber: 1, + expectedPageSize: cdbp.DefaultLimit, + expectedTotal: 1, + }, + { + name: "provider admin with explicit authorized siteId", + reqOrgName: f.ipOrg, + user: f.provUser, + siteID: &f.site.ID, + expectedErr: false, + expectedStatus: http.StatusOK, + expectedNames: []string{f.availTmpl.Name}, + expectedPageNumber: 1, + expectedPageSize: cdbp.DefaultLimit, + expectedTotal: 1, + }, + { + name: "provider admin with unauthorized siteId is forbidden", + reqOrgName: f.ipOrg, + user: f.provUser, + siteID: cutilPtrUUID(uuid.New()), + expectedErr: true, + expectedStatus: http.StatusForbidden, + }, + { + name: "error when siteId is empty", + reqOrgName: f.ipOrg, + user: f.provUser, + queryParams: url.Values{"siteId": {""}}, + expectedErr: true, + expectedStatus: http.StatusBadRequest, + }, + { + name: "error when unknown query parameter is specified", + reqOrgName: f.ipOrg, + user: f.provUser, + queryParams: url.Values{"foo": {"bar"}}, + expectedErr: true, + expectedStatus: http.StatusBadRequest, + }, + { + name: "provider org without any site returns forbidden", + reqOrgName: f.emptyIPOrg, + user: f.provUser, + expectedErr: true, + expectedStatus: http.StatusForbidden, + }, + } + + for _, tc2 := range tests { + t.Run(tc2.name, func(t *testing.T) { + e := echo.New() + q := url.Values{} + if tc2.queryParams != nil { + q = tc2.queryParams + } else if tc2.siteID != nil { + q.Add("siteId", tc2.siteID.String()) + } + path := fmt.Sprintf("/v2/org/%s/nico/ipxe-template?%s", tc2.reqOrgName, q.Encode()) + req := httptest.NewRequest(http.MethodGet, path, nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + ec := e.NewContext(req, rec) + ec.SetParamNames("orgName") + ec.SetParamValues(tc2.reqOrgName) + if tc2.user != nil { + ec.Set("user", tc2.user) + } + ctx = context.WithValue(ctx, otelecho.TracerKey, tracer) + ec.SetRequest(ec.Request().WithContext(ctx)) + + h := NewGetAllIpxeTemplateHandler(f.dbSession, tc, cfg) + err := h.Handle(ec) + assert.Nil(t, err) + require.Equal(t, tc2.expectedStatus, rec.Code) + assert.Equal(t, tc2.expectedErr, rec.Code != http.StatusOK) + + if !tc2.expectedErr { + rsp := []model.APIIpxeTemplate{} + err := json.Unmarshal(rec.Body.Bytes(), &rsp) + assert.Nil(t, err) + gotNames := map[string]bool{} + for _, tmpl := range rsp { + gotNames[tmpl.Name] = true + } + for _, want := range tc2.expectedNames { + assert.True(t, gotNames[want], "expected template %q in response", want) + } + // The orphan template must never be returned. + assert.False(t, gotNames[f.orphanTmpl.Name], "orphan template must not be visible") + + paginationHeader := rec.Header().Get(pagination.ResponseHeaderName) + assert.NotEmpty(t, paginationHeader) + var pageResp pagination.PageResponse + err = json.Unmarshal([]byte(paginationHeader), &pageResp) + require.NoError(t, err) + assert.Equal(t, tc2.expectedPageNumber, pageResp.PageNumber) + assert.Equal(t, tc2.expectedPageSize, pageResp.PageSize) + assert.Equal(t, tc2.expectedTotal, pageResp.Total) + } + }) + } +} + +func TestIpxeTemplateHandler_Get(t *testing.T) { + f, cleanup := buildIpxeTemplateFixture(t) + defer cleanup() + + ctx := context.Background() + cfg := common.GetTestConfig() + tc := &tmocks.Client{} + tracer, _, ctx := common.TestCommonTraceProviderSetup(t, ctx) + + tests := []struct { + name string + reqOrgName string + user *cdbm.User + templateID string + expectedErr bool + expectedStatus int + }{ + { + name: "error when user not in request context", + reqOrgName: f.ipOrg, + user: nil, + templateID: f.availTmpl.ID.String(), + expectedErr: true, + expectedStatus: http.StatusInternalServerError, + }, + { + name: "error when user not a member of org", + reqOrgName: "SomeOtherOrg", + user: f.provUser, + templateID: f.availTmpl.ID.String(), + expectedErr: true, + expectedStatus: http.StatusForbidden, + }, + { + name: "error when template id is not a uuid", + reqOrgName: f.ipOrg, + user: f.provUser, + templateID: "not-a-uuid", + expectedErr: true, + expectedStatus: http.StatusBadRequest, + }, + { + name: "error when template does not exist", + reqOrgName: f.ipOrg, + user: f.provUser, + templateID: uuid.New().String(), + expectedErr: true, + expectedStatus: http.StatusNotFound, + }, + { + name: "forbidden when template not available at any accessible site", + reqOrgName: f.ipOrg, + user: f.provUser, + templateID: f.orphanTmpl.ID.String(), + expectedErr: true, + expectedStatus: http.StatusForbidden, + }, + { + name: "provider admin can retrieve available template", + reqOrgName: f.ipOrg, + user: f.provUser, + templateID: f.availTmpl.ID.String(), + expectedErr: false, + expectedStatus: http.StatusOK, + }, + { + name: "tenant admin can retrieve available template", + reqOrgName: f.tnOrg, + user: f.tnUser, + templateID: f.availTmpl.ID.String(), + expectedErr: false, + expectedStatus: http.StatusOK, + }, + } + + for _, tc2 := range tests { + t.Run(tc2.name, func(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + ec := e.NewContext(req, rec) + ec.SetParamNames("orgName", "id") + ec.SetParamValues(tc2.reqOrgName, tc2.templateID) + if tc2.user != nil { + ec.Set("user", tc2.user) + } + ctx = context.WithValue(ctx, otelecho.TracerKey, tracer) + ec.SetRequest(ec.Request().WithContext(ctx)) + + h := NewGetIpxeTemplateHandler(f.dbSession, tc, cfg) + err := h.Handle(ec) + assert.Nil(t, err) + require.Equal(t, tc2.expectedStatus, rec.Code) + assert.Equal(t, tc2.expectedErr, rec.Code != http.StatusOK) + + if !tc2.expectedErr { + rsp := &model.APIIpxeTemplate{} + err := json.Unmarshal(rec.Body.Bytes(), rsp) + assert.Nil(t, err) + assert.Equal(t, tc2.templateID, rsp.ID) + } + }) + } +} + +func cutilPtrUUID(id uuid.UUID) *uuid.UUID { return &id } diff --git a/rest-api/api/pkg/api/handler/operatingsystem.go b/rest-api/api/pkg/api/handler/operatingsystem.go index 8a4f5bdf99..75128ae184 100644 --- a/rest-api/api/pkg/api/handler/operatingsystem.go +++ b/rest-api/api/pkg/api/handler/operatingsystem.go @@ -14,10 +14,12 @@ import ( "go.opentelemetry.io/otel/attribute" temporalClient "go.temporal.io/sdk/client" tp "go.temporal.io/sdk/temporal" + "google.golang.org/protobuf/proto" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/google/uuid" "github.com/labstack/echo/v4" + "github.com/rs/zerolog" "github.com/NVIDIA/infra-controller/rest-api/api/internal/config" "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/handler/util/common" @@ -34,6 +36,206 @@ import ( "github.com/NVIDIA/infra-controller/rest-api/workflow/pkg/queue" ) +// NICo Core (forge.Forge) Operating System methods proxied for iPXE / Templated +// iPXE Operating Systems via the generic Core gRPC proxy (epic #1927). Image-type +// Operating Systems continue to use the dedicated OsImage site workflows. +const ( + createOperatingSystemMethod = "/forge.Forge/CreateOperatingSystem" + updateOperatingSystemMethod = "/forge.Forge/UpdateOperatingSystem" + deleteOperatingSystemMethod = "/forge.Forge/DeleteOperatingSystem" +) + +// syncOperatingSystemToSitesViaProxy pushes an iPXE / Templated iPXE Operating +// System create or update to each associated site through the generic NICo Core +// gRPC proxy, updating each site association's status (Synced on success, Error +// on failure). The same request proto is sent to every site (the OS definition is +// site-independent). It returns the number of sites that failed to sync. +func syncOperatingSystemToSitesViaProxy( + ctx context.Context, + logger zerolog.Logger, + dbSession *cdb.Session, + scp *sc.ClientPool, + ossas []cdbm.OperatingSystemSiteAssociation, + fullMethod string, + req proto.Message, +) int { + siteErrors := 0 + for _, ossa := range ossas { + slogger := logger.With().Str("Site ID", ossa.SiteID.String()).Logger() + + stc, cerr := scp.GetClientByID(ossa.SiteID) + if cerr != nil { + slogger.Error().Err(cerr).Msg("failed to retrieve Temporal client for Site") + // Site is already counted as an error below; a bookkeeping failure is + // logged inside and does not change the outcome for this site. + _ = updateOSSAStatusViaProxy(ctx, slogger, dbSession, ossa.ID, cdbm.OperatingSystemSiteAssociationStatusError, "failed to connect to site") + siteErrors++ + continue + } + + // The site ID is the shared key used to encrypt any redacted secret fields + // for transport; no top-level secret fields are redacted here (artifact + // authTokens are nested and carried as-is). + perr := common.ExecuteCoreGRPC(ctx, stc, fullMethod, req, nil, ossa.SiteID.String()) + if perr != nil { + slogger.Error().Err(perr).Int("code", perr.Code).Msg("failed to sync Operating System to site via Core proxy") + _ = updateOSSAStatusViaProxy(ctx, slogger, dbSession, ossa.ID, cdbm.OperatingSystemSiteAssociationStatusError, "failed to sync Operating System to site") + siteErrors++ + continue + } + + // The proxy call succeeded, but if we cannot durably record the Synced + // status the association state is unreliable, so treat that as a site + // error rather than reporting the OS as fully synced. + if serr := updateOSSAStatusViaProxy(ctx, slogger, dbSession, ossa.ID, cdbm.OperatingSystemSiteAssociationStatusSynced, "Operating System successfully synced to site"); serr != nil { + siteErrors++ + } + } + return siteErrors +} + +// updateOSSAStatusViaProxy updates an Operating System Site Association status and +// records the corresponding status detail atomically in a single transaction, so +// the status and its audit entry cannot diverge. Any persistence error is logged +// and returned so callers can account for it (e.g. as a site error when computing +// aggregate status) instead of silently reporting success. +func updateOSSAStatusViaProxy(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session, ossaID uuid.UUID, status string, message string) error { + err := cdb.WithTx(ctx, dbSession, func(tx *cdb.Tx) error { + ossaDAO := cdbm.NewOperatingSystemSiteAssociationDAO(dbSession) + sdDAO := cdbm.NewStatusDetailDAO(dbSession) + if _, uerr := ossaDAO.Update(ctx, tx, cdbm.OperatingSystemSiteAssociationUpdateInput{ + OperatingSystemSiteAssociationID: ossaID, + Status: cutil.GetPtr(status), + }); uerr != nil { + return uerr + } + if _, cerr := sdDAO.Create(ctx, tx, cdbm.StatusDetailCreateInput{EntityID: ossaID.String(), Status: status, Message: &message}); cerr != nil { + return cerr + } + return nil + }) + if err != nil { + logger.Error().Err(err).Str("Status", status).Msg("failed to persist Operating System Site Association status") + } + return err +} + +// aggregateSyncMessage returns the status-detail message for a proxy sync aggregate +// status update (create/update), where the outcome is either a full sync or a +// partial/complete sync failure. +func aggregateSyncMessage(hadErrors bool) string { + if hadErrors { + return "failed to sync Operating System to one or more sites" + } + return "Operating System successfully synced to all sites" +} + +// updateOperatingSystemAggregateStatus sets the Operating System's aggregate status +// after a proxy sync attempt: Ready when all sites synced, Error when one or more failed. +// Callers supply the operation-specific status-detail message (e.g. a sync message for +// create/update, a deletion message for delete). The status update and its status-detail +// audit entry are written together in a single transaction so they cannot diverge, and +// any transaction/DB failure is returned to the caller rather than only logged. +// +// A Deactivated Operating System keeps its Deactivated status: the sync still pushes the +// definition to sites, but the aggregate readiness/error status must not override the +// user-initiated deactivation. +func updateOperatingSystemAggregateStatus(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session, osID uuid.UUID, hadErrors bool, message string) error { + osDAO := cdbm.NewOperatingSystemDAO(dbSession) + + status := cdbm.OperatingSystemStatusReady + if hadErrors { + status = cdbm.OperatingSystemStatusError + } + + if err := cdb.WithTx(ctx, dbSession, func(tx *cdb.Tx) error { + // Serialize with other Operating System state changes (deactivation, + // deletion) via the per-OS advisory lock, held for the life of this + // transaction. This closes the read-then-write race: the deactivation guard + // re-reads the status under the lock, so a concurrent Deactivate cannot land + // between the read and the write and be clobbered by Ready/Error. + if lerr := tx.TryAcquireAdvisoryLock(ctx, cdb.GetAdvisoryLockIDFromString(osID.String()), nil); lerr != nil { + logger.Error().Err(lerr).Msg("failed to acquire advisory lock on Operating System for aggregate status update") + return lerr + } + + existing, gerr := osDAO.GetByID(ctx, tx, osID, nil) + if gerr != nil { + logger.Error().Err(gerr).Msg("failed to read Operating System before aggregate status update") + return gerr + } + // A Deactivated Operating System keeps its Deactivated status: the sync still + // pushed the definition to sites, but the aggregate readiness/error status + // must not override the user-initiated deactivation. + if existing.Status == cdbm.OperatingSystemStatusDeactivated { + logger.Info().Msg("Operating System is deactivated, preserving Deactivated status after site sync") + return nil + } + + if _, uerr := osDAO.Update(ctx, tx, cdbm.OperatingSystemUpdateInput{OperatingSystemId: osID, Status: &status}); uerr != nil { + return uerr + } + sdDAO := cdbm.NewStatusDetailDAO(dbSession) + if _, cerr := sdDAO.Create(ctx, tx, cdbm.StatusDetailCreateInput{EntityID: osID.String(), Status: status, Message: &message}); cerr != nil { + return cerr + } + return nil + }); err != nil { + logger.Error().Err(err).Msg("failed to persist aggregate Operating System status") + return err + } + return nil +} + +// validateIpxeTemplateAvailableAtSites verifies the referenced iPXE template is +// available (has an IpxeTemplateSiteAssociation) at every one of the given Sites. +// A Templated iPXE Operating System is rendered on the Site from its template, so +// per the OS sync contract it can only be synced to a Site whose Site currently has +// that template; creating or updating one for a Site that lacks it would persist a +// definition that can never render there. templateID must be a valid template UUID; +// a non-existent template (no associations anywhere) is reported as unavailable at +// every Site. An empty site set is a no-op. +func validateIpxeTemplateAvailableAtSites(ctx context.Context, dbSession *cdb.Session, logger zerolog.Logger, templateID string, siteIDs []uuid.UUID) *cutil.APIError { + if len(siteIDs) == 0 { + return nil + } + + tid, perr := uuid.Parse(templateID) + if perr != nil { + return cutil.NewAPIError(http.StatusBadRequest, "iPXE template ID specified in request is not a valid UUID", nil) + } + + itsaDAO := cdbm.NewIpxeTemplateSiteAssociationDAO(dbSession) + itsas, _, err := itsaDAO.GetAll(ctx, nil, + cdbm.IpxeTemplateSiteAssociationFilterInput{ + IpxeTemplateIDs: []uuid.UUID{tid}, + SiteIDs: siteIDs, + }, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if err != nil { + logger.Error().Err(err).Str("ipxeTemplateId", templateID).Msg("error retrieving iPXE template Site associations for Operating System validation") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to validate iPXE template availability, DB error", nil) + } + + available := make(map[uuid.UUID]struct{}, len(itsas)) + for _, a := range itsas { + available[a.SiteID] = struct{}{} + } + missing := make([]string, 0) + for _, sid := range siteIDs { + if _, ok := available[sid]; !ok { + missing = append(missing, sid.String()) + } + } + if len(missing) > 0 { + logger.Warn().Str("ipxeTemplateId", templateID).Msg("iPXE template is not available at one or more target Sites") + return cutil.NewAPIError(http.StatusBadRequest, fmt.Sprintf("iPXE template %s specified in request is not available at Site(s): %v", templateID, missing), nil) + } + return nil +} + // ~~~~~ Create Handler ~~~~~ // // CreateOperatingSystemHandler is the API Handler for creating new OperatingSystem @@ -159,11 +361,9 @@ func (csh CreateOperatingSystemHandler) Handle(c echo.Context) error { }) } - // check OS type from request - osType := cdbm.OperatingSystemTypeImage - if apiRequest.IpxeScript != nil { - osType = cdbm.OperatingSystemTypeIPXE - } + // Infer OS type from the provided source fields (ipxeScript -> iPXE, + // ipxeTemplateId -> Templated iPXE, otherwise Image). + osType := apiRequest.GetOperatingSystemType() // Set the phoneHomeEnabled if provided in request phoneHomeEnabled := false @@ -232,10 +432,26 @@ func (csh CreateOperatingSystemHandler) Handle(c echo.Context) error { rdbst = append(rdbst, *site) } - // Create status based on OS type + // A Templated iPXE Operating System is rendered on-Site from its iPXE template, + // so it can only be synced to Sites that currently have the template. Reject up + // front if the referenced template is unavailable at any target Site rather than + // persist a definition that can never render there. + if osType == cdbm.OperatingSystemTypeTemplatedIPXE { + targetSiteIDs := make([]uuid.UUID, 0, len(rdbst)) + for i := range rdbst { + targetSiteIDs = append(targetSiteIDs, rdbst[i].ID) + } + if apiErr := validateIpxeTemplateAvailableAtSites(ctx, csh.dbSession, logger, *apiRequest.IpxeTemplateId, targetSiteIDs); apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) + } + } + + // Create status based on OS type. iPXE / Templated iPXE definitions are pushed + // to sites after commit, so Templated iPXE starts in Syncing; raw iPXE (no site + // associations) is immediately Ready. osStatus := cdbm.OperatingSystemStatusReady osStatusMessage := "Operating System is ready for use" - if osType == cdbm.OperatingSystemTypeImage { + if osType == cdbm.OperatingSystemTypeImage || osType == cdbm.OperatingSystemTypeTemplatedIPXE { osStatus = cdbm.OperatingSystemStatusSyncing osStatusMessage = "received Operating System creation request, syncing" } @@ -251,25 +467,28 @@ func (csh CreateOperatingSystemHandler) Handle(c echo.Context) error { err = cdb.WithTx(ctx, csh.dbSession, func(tx *cdb.Tx) error { // Create the db record for Operating System osInput := cdbm.OperatingSystemCreateInput{ - Name: apiRequest.Name, - Description: apiRequest.Description, - Org: org, - TenantID: &tenant.ID, - OsType: osType, - ImageURL: apiRequest.ImageURL, - ImageSHA: apiRequest.ImageSHA, - ImageAuthType: apiRequest.ImageAuthType, - ImageAuthToken: apiRequest.ImageAuthToken, - ImageDisk: apiRequest.ImageDisk, - RootFsId: apiRequest.RootFsID, - RootFsLabel: apiRequest.RootFsLabel, - IpxeScript: apiRequest.IpxeScript, - UserData: apiRequest.UserData, - AllowOverride: apiRequest.AllowOverride, - EnableBlockStorage: apiRequest.EnableBlockStorage, - PhoneHomeEnabled: phoneHomeEnabled, - Status: osStatus, - CreatedBy: dbUser.ID, + Name: apiRequest.Name, + Description: apiRequest.Description, + Org: org, + TenantID: &tenant.ID, + OsType: osType, + ImageURL: apiRequest.ImageURL, + ImageSHA: apiRequest.ImageSHA, + ImageAuthType: apiRequest.ImageAuthType, + ImageAuthToken: apiRequest.ImageAuthToken, + ImageDisk: apiRequest.ImageDisk, + RootFsId: apiRequest.RootFsID, + RootFsLabel: apiRequest.RootFsLabel, + IpxeScript: apiRequest.IpxeScript, + IpxeTemplateId: apiRequest.IpxeTemplateId, + IpxeTemplateParameters: apiRequest.IpxeTemplateParameters.ToDBModel(), + IpxeTemplateArtifacts: apiRequest.IpxeTemplateArtifacts.ToDBModel(), + UserData: apiRequest.UserData, + AllowOverride: apiRequest.AllowOverride, + EnableBlockStorage: apiRequest.EnableBlockStorage, + PhoneHomeEnabled: phoneHomeEnabled, + Status: osStatus, + CreatedBy: dbUser.ID, } createdOs, derr := osDAO.Create(ctx, tx, osInput) if derr != nil { @@ -344,8 +563,13 @@ func (csh CreateOperatingSystemHandler) Handle(c echo.Context) error { } dbossa = retossa - // Trigger workflows to sync Image based Operating System with various Sites + // Trigger workflows to sync Image based Operating System with various Sites. + // iPXE / Templated iPXE definitions are pushed to sites via the Core gRPC + // proxy after the transaction commits (see below), so they are skipped here. for _, ossa := range dbossa { + if os.Type != cdbm.OperatingSystemTypeImage { + continue + } // Iteration body wrapped in a function literal so `defer cancel()` // scopes to the iteration; otherwise the deferred cancels would // pile up until the WithTx closure returns. @@ -421,12 +645,61 @@ func (csh CreateOperatingSystemHandler) Handle(c echo.Context) error { return timeoutResp() } + // Push iPXE / Templated iPXE Operating Systems to associated sites through the + // generic Core gRPC proxy (Image OSes are synced in-transaction above). Per-site + // failures are recorded on the association status and do not fail the request. + if cdbm.IsIPXEType(os.Type) && len(dbossa) > 0 { + req := model.BuildCreateOperatingSystemRequest(os) + siteErrors := syncOperatingSystemToSitesViaProxy(ctx, logger, csh.dbSession, csh.scp, dbossa, createOperatingSystemMethod, req) + if aerr := updateOperatingSystemAggregateStatus(ctx, logger, csh.dbSession, os.ID, siteErrors > 0, aggregateSyncMessage(siteErrors > 0)); aerr != nil { + logger.Error().Err(aerr).Msg("failed to update aggregate Operating System status after create sync") + } + os, dbossd, dbossa = reloadOperatingSystemForResponse(ctx, logger, csh.dbSession, os, dbossd, dbossa) + } + // create response apiOperatingSystem := model.NewAPIOperatingSystem(os, dbossd, dbossa, sttsmap) logger.Info().Msg("finishing API handler") return c.JSON(http.StatusCreated, apiOperatingSystem) } +// reloadOperatingSystemForResponse re-reads the Operating System, its recent status +// details, and its site associations after a proxy sync so the API response reflects +// the post-sync state. Best-effort: the caller passes the values it already holds +// (priorSSDs, priorOSSAs), and each is replaced only on a successful re-read, so a +// read error keeps the prior value rather than dropping it to nil. +func reloadOperatingSystemForResponse(ctx context.Context, logger zerolog.Logger, dbSession *cdb.Session, os *cdbm.OperatingSystem, priorSSDs []cdbm.StatusDetail, priorOSSAs []cdbm.OperatingSystemSiteAssociation) (*cdbm.OperatingSystem, []cdbm.StatusDetail, []cdbm.OperatingSystemSiteAssociation) { + osDAO := cdbm.NewOperatingSystemDAO(dbSession) + ossaDAO := cdbm.NewOperatingSystemSiteAssociationDAO(dbSession) + sdDAO := cdbm.NewStatusDetailDAO(dbSession) + + reloadedOS := os + if v, err := osDAO.GetByID(ctx, nil, os.ID, nil); err == nil { + reloadedOS = v + } else { + logger.Warn().Err(err).Msg("failed to reload Operating System for response") + } + + ssds := priorSSDs + if v, err := sdDAO.GetRecentByEntityIDs(ctx, nil, []string{os.ID.String()}, common.RECENT_STATUS_DETAIL_COUNT); err == nil { + ssds = v + } else { + logger.Warn().Err(err).Msg("failed to reload Operating System status details for response") + } + + ossas := priorOSSAs + if v, _, err := ossaDAO.GetAll(ctx, nil, + cdbm.OperatingSystemSiteAssociationFilterInput{OperatingSystemIDs: []uuid.UUID{os.ID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + []string{cdbm.SiteRelationName}); err == nil { + ossas = v + } else { + logger.Warn().Err(err).Msg("failed to reload Operating System site associations for response") + } + + return reloadedOS, ssds, ossas +} + // ~~~~~ GetAll Handler ~~~~~ // // GetAllOperatingSystemHandler is the API Handler for getting all OperatingSystems @@ -691,10 +964,6 @@ func (gash GetAllOperatingSystemHandler) Handle(c echo.Context) error { apiOperatingSystems := []*model.APIOperatingSystem{} for _, os := range oss { - if os.Type == cdbm.OperatingSystemTypeImage { - fmt.Printf("Processing Operating System: %s, Type: %s\n", os.Name, os.Type) - } - curVal := os apiOperatingSystem := model.NewAPIOperatingSystem(&curVal, ssdMap[os.ID.String()], dbossaMap[os.ID], sttsmap) apiOperatingSystems = append(apiOperatingSystems, apiOperatingSystem) @@ -1087,6 +1356,34 @@ func (ush UpdateOperatingSystemHandler) Handle(c echo.Context) error { } } + // For a Templated iPXE Operating System, verify the effective iPXE template (the + // request's template when changing it, otherwise the current one) is available at + // every Site the OS is synced to before updating and re-pushing it. This mirrors + // the create-time check and also catches a request switching to a template that + // is not present at the OS's Sites. + if os.Type == cdbm.OperatingSystemTypeTemplatedIPXE { + templatedOssas, _, oerr := ossaDAO.GetAll(ctx, nil, + cdbm.OperatingSystemSiteAssociationFilterInput{OperatingSystemIDs: []uuid.UUID{os.ID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, nil) + if oerr != nil { + logger.Error().Err(oerr).Msg("error retrieving Operating System Site associations for iPXE template validation") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Operating System Site associations from DB", nil) + } + effectiveTemplateID := os.IpxeTemplateId + if apiRequest.IpxeTemplateId != nil { + effectiveTemplateID = apiRequest.IpxeTemplateId + } + if effectiveTemplateID != nil && len(templatedOssas) > 0 { + targetSiteIDs := make([]uuid.UUID, 0, len(templatedOssas)) + for _, o := range templatedOssas { + targetSiteIDs = append(targetSiteIDs, o.SiteID) + } + if apiErr := validateIpxeTemplateAvailableAtSites(ctx, ush.dbSession, logger, *effectiveTemplateID, targetSiteIDs); apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) + } + } + } + // Save update status in DB osStatus := cutil.GetPtr(cdbm.OperatingSystemStatusReady) osStatusMessage := "Operating System has been updated and ready for use" @@ -1100,7 +1397,7 @@ func (ush UpdateOperatingSystemHandler) Handle(c echo.Context) error { if apiRequest.IsActive != nil && *apiRequest.IsActive { osStatusMessage = "Operating System has been reactivated and is ready for use" } - if os.Type == cdbm.OperatingSystemTypeImage { + if os.Type == cdbm.OperatingSystemTypeImage || os.Type == cdbm.OperatingSystemTypeTemplatedIPXE { osStatus = cutil.GetPtr(cdbm.OperatingSystemStatusSyncing) osStatusMessage = "received Operating System update request, syncing" } @@ -1126,23 +1423,26 @@ func (ush UpdateOperatingSystemHandler) Handle(c echo.Context) error { } } updatedOs, derr := osDAO.Update(ctx, tx, cdbm.OperatingSystemUpdateInput{ - OperatingSystemId: osID, - Name: apiRequest.Name, - Description: apiRequest.Description, - ImageURL: apiRequest.ImageURL, - ImageSHA: apiRequest.ImageSHA, - ImageAuthType: apiRequest.ImageAuthType, - ImageAuthToken: apiRequest.ImageAuthToken, - ImageDisk: apiRequest.ImageDisk, - RootFsId: apiRequest.RootFsID, - RootFsLabel: apiRequest.RootFsLabel, - IpxeScript: apiRequest.IpxeScript, - UserData: apiRequest.UserData, - AllowOverride: apiRequest.AllowOverride, - PhoneHomeEnabled: apiRequest.PhoneHomeEnabled, - IsActive: apiRequest.IsActive, - DeactivationNote: deactivationNote, - Status: osStatus, + OperatingSystemId: osID, + Name: apiRequest.Name, + Description: apiRequest.Description, + ImageURL: apiRequest.ImageURL, + ImageSHA: apiRequest.ImageSHA, + ImageAuthType: apiRequest.ImageAuthType, + ImageAuthToken: apiRequest.ImageAuthToken, + ImageDisk: apiRequest.ImageDisk, + RootFsId: apiRequest.RootFsID, + RootFsLabel: apiRequest.RootFsLabel, + IpxeScript: apiRequest.IpxeScript, + IpxeTemplateId: apiRequest.IpxeTemplateId, + IpxeTemplateParameters: apiRequest.IpxeTemplateParameters.ToDBModelPtr(), + IpxeTemplateArtifacts: apiRequest.IpxeTemplateArtifacts.ToDBModelPtr(), + UserData: apiRequest.UserData, + AllowOverride: apiRequest.AllowOverride, + PhoneHomeEnabled: apiRequest.PhoneHomeEnabled, + IsActive: apiRequest.IsActive, + DeactivationNote: deactivationNote, + Status: osStatus, }) if derr != nil { logger.Error().Err(derr).Msg("error updating Operating System in DB") @@ -1277,6 +1577,44 @@ func (ush UpdateOperatingSystemHandler) Handle(c echo.Context) error { dbossas = refreshedOssas } + // Templated iPXE updates re-push the definition to every associated Site via + // the Core proxy after commit. Mark each association (and its status detail) + // Syncing inside this transaction so the in-flight state is durable before any + // proxy update runs: validateTemplatedIpxeOsForSite gates Instance selection on + // a Synced association, so this prevents an Instance from being created against a + // definition that is mid-update. The post-commit proxy sync transitions each + // association to Synced or Error. + if uos.Type == cdbm.OperatingSystemTypeTemplatedIPXE { + tmplOssas, _, derr := ossaDAO.GetAll( + ctx, + tx, + cdbm.OperatingSystemSiteAssociationFilterInput{OperatingSystemIDs: []uuid.UUID{uos.ID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + if derr != nil { + logger.Error().Err(derr).Msg("error retrieving Operating System Site associations for templated iPXE update") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to retrieve Operating System Site associations, DB error", nil) + } + for _, tossa := range tmplOssas { + if _, derr := ossaDAO.Update(ctx, tx, cdbm.OperatingSystemSiteAssociationUpdateInput{ + OperatingSystemSiteAssociationID: tossa.ID, + Status: cutil.GetPtr(cdbm.OperatingSystemSiteAssociationStatusSyncing), + }); derr != nil { + logger.Error().Err(derr).Msg("unable to update the Operating System association record in DB") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to update Operating System Site Association status, DB error", nil) + } + if _, derr := sdDAO.Create(ctx, tx, cdbm.StatusDetailCreateInput{ + EntityID: tossa.ID.String(), + Status: cdbm.OperatingSystemSiteAssociationStatusSyncing, + Message: cutil.GetPtr("received Operating System Association update request, syncing"), + }); derr != nil { + logger.Error().Err(derr).Msg("error creating Status Detail DB entry") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to create Status Detail for Operating System Site Association", nil) + } + } + } + return nil }) // The wrapping `if err != nil` ensures real tx-helper errors (commit / @@ -1293,6 +1631,28 @@ func (ush UpdateOperatingSystemHandler) Handle(c echo.Context) error { return timeoutResp() } + // Push iPXE / Templated iPXE Operating System updates to associated sites via the + // generic Core gRPC proxy (Image OSes are synced in-transaction above). Raw iPXE + // OSes without site associations have nothing to push. + if cdbm.IsIPXEType(uos.Type) { + ipxeOssas, _, oerr := ossaDAO.GetAll(ctx, nil, + cdbm.OperatingSystemSiteAssociationFilterInput{OperatingSystemIDs: []uuid.UUID{uos.ID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + []string{cdbm.SiteRelationName}) + if oerr != nil { + logger.Error().Err(oerr).Msg("error retrieving Operating System Site associations for proxy sync") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Operating System Site associations from DB", nil) + } + if len(ipxeOssas) > 0 { + req := model.BuildUpdateOperatingSystemRequest(uos) + siteErrors := syncOperatingSystemToSitesViaProxy(ctx, logger, ush.dbSession, ush.scp, ipxeOssas, updateOperatingSystemMethod, req) + if aerr := updateOperatingSystemAggregateStatus(ctx, logger, ush.dbSession, uos.ID, siteErrors > 0, aggregateSyncMessage(siteErrors > 0)); aerr != nil { + logger.Error().Err(aerr).Msg("failed to update aggregate Operating System status after update sync") + } + uos, ssds, dbossas = reloadOperatingSystemForResponse(ctx, logger, ush.dbSession, uos, ssds, dbossas) + } + } + // Send response apiOperatingSystem := model.NewAPIOperatingSystem(uos, ssds, dbossas, sttsmap) logger.Info().Msg("finishing API handler") @@ -1403,7 +1763,10 @@ func (dsh DeleteOperatingSystemHandler) Handle(c echo.Context) error { // Verify if Site is in Registered state ossaDAO := cdbm.NewOperatingSystemSiteAssociationDAO(dsh.dbSession) ossasToDelete := []cdbm.OperatingSystemSiteAssociation{} - if os.Type == cdbm.OperatingSystemTypeImage { + // Image and Templated iPXE Operating Systems propagate deletes to their + // associated sites (Image via OsImage workflows, Templated iPXE via the Core + // gRPC proxy), so their associations must be loaded. + if os.Type == cdbm.OperatingSystemTypeImage || os.Type == cdbm.OperatingSystemTypeTemplatedIPXE { ossasToDelete, _, err = ossaDAO.GetAll( ctx, nil, @@ -1418,11 +1781,13 @@ func (dsh DeleteOperatingSystemHandler) Handle(c echo.Context) error { return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to retrieve Operating System Site associations from DB", nil) } - // Verify if associated Site is not registered state - for _, dbosa := range ossasToDelete { - if dbosa.Site.Status != cdbm.SiteStatusRegistered { - logger.Warn().Msg(fmt.Sprintf("unable to delete Operating System. Site: %s. is not in Registered state", dbosa.SiteID.String())) - return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Failed to delete Operating System, Associated Site: %s is not in Registered state", dbosa.Site.Name), nil) + // Verify if associated Site is not registered state (image-based only). + if os.Type == cdbm.OperatingSystemTypeImage { + for _, dbosa := range ossasToDelete { + if dbosa.Site.Status != cdbm.SiteStatusRegistered { + logger.Warn().Msg(fmt.Sprintf("unable to delete Operating System. Site: %s. is not in Registered state", dbosa.SiteID.String())) + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, fmt.Sprintf("Failed to delete Operating System, Associated Site: %s is not in Registered state", dbosa.Site.Name), nil) + } } } } @@ -1572,6 +1937,50 @@ func (dsh DeleteOperatingSystemHandler) Handle(c echo.Context) error { } } + // Templated iPXE Operating Systems mark the OS (and associations) as + // Deleting in-transaction; the deletes are pushed to sites via the Core + // gRPC proxy after commit, and the OS is soft-deleted once all sites are + // cleaned up. + if os.Type == cdbm.OperatingSystemTypeTemplatedIPXE && len(ossasToDelete) > 0 { + if _, derr := osDAO.Update(ctx, tx, cdbm.OperatingSystemUpdateInput{OperatingSystemId: os.ID, Status: cutil.GetPtr(cdbm.OperatingSystemStatusDeleting)}); derr != nil { + logger.Error().Err(derr).Msg("error updating Operating System in DB") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to delete Operating System", nil) + } + sdDAO := cdbm.NewStatusDetailDAO(dsh.dbSession) + if _, derr := sdDAO.Create(ctx, tx, cdbm.StatusDetailCreateInput{ + EntityID: os.ID.String(), + Status: cdbm.OperatingSystemStatusDeleting, + Message: cutil.GetPtr("received request for deletion, pending processing"), + }); derr != nil { + logger.Error().Err(derr).Msg("error creating Status Detail DB entry") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to create Status Detail for Operating System", nil) + } + + // Mark each affected association Deleting in the same transaction so the + // in-progress state is durable before any per-site proxy delete runs; the + // post-commit loop transitions each to deleted (row removed) or Error. + for _, ossa := range ossasToDelete { + if ossa.Status == cdbm.OperatingSystemSiteAssociationStatusDeleting { + continue + } + if _, derr := ossaDAO.Update(ctx, tx, cdbm.OperatingSystemSiteAssociationUpdateInput{ + OperatingSystemSiteAssociationID: ossa.ID, + Status: cutil.GetPtr(cdbm.OperatingSystemSiteAssociationStatusDeleting), + }); derr != nil { + logger.Error().Err(derr).Msg("error updating Operating System Association in DB") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to delete Operating System", nil) + } + if _, derr := sdDAO.Create(ctx, tx, cdbm.StatusDetailCreateInput{ + EntityID: ossa.ID.String(), + Status: cdbm.OperatingSystemSiteAssociationStatusDeleting, + Message: cutil.GetPtr("received request for deletion, pending processing"), + }); derr != nil { + logger.Error().Err(derr).Msg("error creating Status Detail DB entry") + return cutil.NewAPIError(http.StatusInternalServerError, "Failed to create Status Detail for Operating System Association", nil) + } + } + } + // Delete OS if its not Image // Delete OS if there is no Operating Site Association in case of Image based OS if os.Type == cdbm.OperatingSystemTypeIPXE || len(ossasToDelete) == 0 { @@ -1598,6 +2007,57 @@ func (dsh DeleteOperatingSystemHandler) Handle(c echo.Context) error { return timeoutResp() } + // Push deletes for Templated iPXE Operating Systems to associated sites via the + // generic Core gRPC proxy, remove the synced associations, and soft-delete the + // OS once every site is cleaned up. A not-found object on a site is treated as + // already deleted. + if os.Type == cdbm.OperatingSystemTypeTemplatedIPXE && len(ossasToDelete) > 0 { + req := model.BuildDeleteOperatingSystemRequest(os) + remaining := 0 + for _, ossa := range ossasToDelete { + slogger := logger.With().Str("Site ID", ossa.SiteID.String()).Logger() + stc, cerr := dsh.scp.GetClientByID(ossa.SiteID) + if cerr != nil { + slogger.Error().Err(cerr).Msg("failed to retrieve Temporal client for Site") + _ = updateOSSAStatusViaProxy(ctx, slogger, dsh.dbSession, ossa.ID, cdbm.OperatingSystemSiteAssociationStatusError, "failed to connect to site") + remaining++ + continue + } + perr := common.ExecuteCoreGRPC(ctx, stc, deleteOperatingSystemMethod, req, nil, ossa.SiteID.String()) + if perr != nil { + if perr.Code == http.StatusNotFound { + slogger.Warn().Msg("Operating System not found on site, treating delete as successful") + } else { + slogger.Error().Err(perr).Int("code", perr.Code).Msg("failed to delete Operating System on site via Core proxy") + _ = updateOSSAStatusViaProxy(ctx, slogger, dsh.dbSession, ossa.ID, cdbm.OperatingSystemSiteAssociationStatusError, "failed to delete Operating System on site") + remaining++ + continue + } + } + if derr := ossaDAO.Delete(ctx, nil, ossa.ID); derr != nil { + slogger.Error().Err(derr).Msg("failed to delete Operating System Site Association after site delete") + _ = updateOSSAStatusViaProxy(ctx, slogger, dsh.dbSession, ossa.ID, cdbm.OperatingSystemSiteAssociationStatusError, "failed to remove Operating System site association after site delete") + remaining++ + } + } + if remaining == 0 { + if derr := osDAO.Delete(ctx, nil, os.ID); derr != nil { + logger.Error().Err(derr).Msg("failed to soft-delete Operating System after all sites cleaned up") + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to delete Operating System", nil) + } + } else { + // Some sites could not be cleaned up. Transition the OS out of the + // transient Deleting state into Error (with per-site Error associations + // recorded above) so it is not stranded in Deleting forever: the state is + // visible via GET and the delete is idempotently retryable (already-gone + // sites return not-found and cleaned-up associations are no longer + // candidates), converging once the sites recover. + if aerr := updateOperatingSystemAggregateStatus(ctx, logger, dsh.dbSession, os.ID, true, "failed to delete Operating System from one or more sites"); aerr != nil { + return cutil.NewAPIErrorResponse(c, http.StatusInternalServerError, "Failed to update Operating System status", nil) + } + } + } + // Create response logger.Info().Msg("finishing API handler") return c.JSON(http.StatusAccepted, model.NewAPIDeletionAcceptedResponse()) diff --git a/rest-api/api/pkg/api/handler/operatingsystem_templated_proxy_test.go b/rest-api/api/pkg/api/handler/operatingsystem_templated_proxy_test.go new file mode 100644 index 0000000000..ca86d851ba --- /dev/null +++ b/rest-api/api/pkg/api/handler/operatingsystem_templated_proxy_test.go @@ -0,0 +1,438 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package handler + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/NVIDIA/infra-controller/rest-api/api/internal/config" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/handler/util/common" + "github.com/NVIDIA/infra-controller/rest-api/api/pkg/api/model" + sc "github.com/NVIDIA/infra-controller/rest-api/api/pkg/client/site" + authz "github.com/NVIDIA/infra-controller/rest-api/auth/pkg/authorization" + "github.com/NVIDIA/infra-controller/rest-api/common/pkg/coreproxy" + "github.com/NVIDIA/infra-controller/rest-api/common/pkg/otelecho" + cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" + cdb "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db" + cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" + cdbp "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/paginator" + corev1 "github.com/NVIDIA/infra-controller/rest-api/proto/core/gen/v1" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + enums "go.temporal.io/api/enums/v1" + tmocks "go.temporal.io/sdk/mocks" + tp "go.temporal.io/sdk/temporal" + "google.golang.org/protobuf/encoding/protojson" +) + +// templatedProxyFixture wires up a tenant-owned Templated iPXE OS test +// environment with one registered site and a Public template available there. +type templatedProxyFixture struct { + ctx context.Context + dbSession *cdb.Session + cfg *config.Config + scp *sc.ClientPool + tc *tmocks.Client + tracer trace.Tracer + + ipOrg string + tnOrg string + tnu *cdbm.User + site *cdbm.Site + tmpl *cdbm.IpxeTemplate +} + +func buildTemplatedProxyFixture(t *testing.T) *templatedProxyFixture { + t.Helper() + + ctx := context.Background() + dbSession := testMachineInitDB(t) + common.TestSetupSchema(t, dbSession) + + cfg := common.GetTestConfig() + + ipOrg := "tmpl-proxy-ip-org" + tnOrg := "tmpl-proxy-tn-org" + + ipu := testMachineBuildUser(t, dbSession, uuid.NewString(), []string{ipOrg}, []string{authz.ProviderAdminRole}) + _ = ipu + tnu := testMachineBuildUser(t, dbSession, uuid.NewString(), []string{tnOrg}, []string{authz.TenantAdminRole}) + + ip := testMachineBuildInfrastructureProvider(t, dbSession, ipOrg, "tmpl-proxy-provider") + site := testMachineBuildSite(t, dbSession, ip, "tmpl-proxy-site", cdbm.SiteStatusRegistered) + + tenant := testMachineBuildTenant(t, dbSession, tnOrg, "tmpl-proxy-tenant") + testBuildTenantSiteAssociation(t, dbSession, tnOrg, tenant.ID, site.ID, tnu.ID) + + templateDAO := cdbm.NewIpxeTemplateDAO(dbSession) + tmpl, err := templateDAO.Create(ctx, nil, cdbm.IpxeTemplateCreateInput{ + ID: uuid.New(), + Name: "tmpl-proxy-template", + Template: "#!ipxe\n", + Visibility: "Public", + }) + require.NoError(t, err) + itsaDAO := cdbm.NewIpxeTemplateSiteAssociationDAO(dbSession) + _, err = itsaDAO.Create(ctx, nil, cdbm.IpxeTemplateSiteAssociationCreateInput{IpxeTemplateID: tmpl.ID, SiteID: site.ID}) + require.NoError(t, err) + + tracer, _, ctx := common.TestCommonTraceProviderSetup(t, ctx) + + tcfg, _ := cfg.GetTemporalConfig() + scp := sc.NewClientPool(tcfg) + + t.Cleanup(func() { dbSession.Close() }) + + return &templatedProxyFixture{ + ctx: ctx, + dbSession: dbSession, + cfg: cfg, + scp: scp, + tc: &tmocks.Client{}, + tracer: tracer, + ipOrg: ipOrg, + tnOrg: tnOrg, + tnu: tnu, + site: site, + tmpl: tmpl, + } +} + +type proxySiteClient struct { + client *tmocks.Client + workflow *tmocks.WorkflowRun + captured coreproxy.Request +} + +func newProxySiteClient(t *testing.T, wantMethod string, getErr, executeErr error) *proxySiteClient { + t.Helper() + + psc := &proxySiteClient{ + workflow: &tmocks.WorkflowRun{}, + client: &tmocks.Client{}, + } + psc.client.On( + "ExecuteWorkflow", + mock.Anything, + mock.Anything, + coreproxy.WorkflowName, + mock.MatchedBy(func(req coreproxy.Request) bool { + if req.FullMethod != wantMethod { + return false + } + psc.captured = req + return true + }), + ).Return(psc.workflow, executeErr).Once() + if executeErr == nil { + psc.workflow.On("Get", mock.Anything, mock.Anything).Return(getErr).Once() + } + + return psc +} + +func (f *templatedProxyFixture) bindProxyClient(psc *proxySiteClient) { + f.scp.IDClientMap[f.site.ID.String()] = psc.client +} + +func (f *templatedProxyFixture) newEchoContext(method, body string, params map[string]string) (echo.Context, *httptest.ResponseRecorder) { + e := echo.New() + req := httptest.NewRequest(method, "/", strings.NewReader(body)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + ec := e.NewContext(req, rec) + names := make([]string, 0, len(params)) + values := make([]string, 0, len(params)) + for k, v := range params { + names = append(names, k) + values = append(values, v) + } + ec.SetParamNames(names...) + ec.SetParamValues(values...) + ec.Set("user", f.tnu) + reqCtx := context.WithValue(f.ctx, otelecho.TracerKey, f.tracer) + ec.SetRequest(ec.Request().WithContext(reqCtx)) + return ec, rec +} + +func assertProxyCreatePayload(t *testing.T, tnOrg, tmplID string, reqJSON []byte, wantVersion string) { + t.Helper() + var coreReq corev1.CreateOperatingSystemRequest + require.NoError(t, protojson.Unmarshal(reqJSON, &coreReq)) + assert.Equal(t, "tmpl-proxy-os", coreReq.Name) + assert.Equal(t, tnOrg, coreReq.GetTenantOrganizationId()) + assert.Equal(t, tmplID, coreReq.GetIpxeTemplateId().GetValue()) + require.Len(t, coreReq.IpxeTemplateParameters, 1) + assert.Equal(t, "version", coreReq.IpxeTemplateParameters[0].Name) + assert.Equal(t, wantVersion, coreReq.IpxeTemplateParameters[0].Value) + assert.NotEmpty(t, coreReq.GetId().GetValue()) +} + +func assertProxyUpdatePayload(t *testing.T, osID, tmplID string, reqJSON []byte, wantVersion string) { + t.Helper() + var coreReq corev1.UpdateOperatingSystemRequest + require.NoError(t, protojson.Unmarshal(reqJSON, &coreReq)) + assert.Equal(t, osID, coreReq.GetId().GetValue()) + assert.Equal(t, tmplID, coreReq.GetIpxeTemplateId().GetValue()) + require.NotNil(t, coreReq.IpxeTemplateParameters) + require.Len(t, coreReq.IpxeTemplateParameters.Items, 1) + assert.Equal(t, "version", coreReq.IpxeTemplateParameters.Items[0].Name) + assert.Equal(t, wantVersion, coreReq.IpxeTemplateParameters.Items[0].Value) +} + +func assertProxyDeletePayload(t *testing.T, osID string, reqJSON []byte) { + t.Helper() + var coreReq corev1.DeleteOperatingSystemRequest + require.NoError(t, protojson.Unmarshal(reqJSON, &coreReq)) + assert.Equal(t, osID, coreReq.GetId().GetValue()) +} + +func (f *templatedProxyFixture) osAssociationStatus(t *testing.T, osID uuid.UUID) string { + t.Helper() + ossaDAO := cdbm.NewOperatingSystemSiteAssociationDAO(f.dbSession) + ossas, _, err := ossaDAO.GetAll(f.ctx, nil, + cdbm.OperatingSystemSiteAssociationFilterInput{OperatingSystemIDs: []uuid.UUID{osID}}, + cdbp.PageInput{Limit: cutil.GetPtr(cdbp.TotalLimit)}, + nil, + ) + require.NoError(t, err) + require.Len(t, ossas, 1) + return ossas[0].Status +} + +func (f *templatedProxyFixture) osStatus(t *testing.T, osID uuid.UUID) string { + t.Helper() + osDAO := cdbm.NewOperatingSystemDAO(f.dbSession) + os, err := osDAO.GetByID(f.ctx, nil, osID, nil) + require.NoError(t, err) + return os.Status +} + +// TestOperatingSystemHandler_TemplatedIPXE_Proxy exercises the full create / +// update / delete lifecycle of a Templated iPXE Operating System, which is +// synchronized to its associated Sites through the generic NICo Core gRPC proxy +// (coreproxy.WorkflowName) rather than the dedicated OsImage workflows used by +// Image based Operating Systems. +func TestOperatingSystemHandler_TemplatedIPXE_Proxy(t *testing.T) { + f := buildTemplatedProxyFixture(t) + + var osID string + + t.Run("create Templated iPXE syncs to sites via proxy", func(t *testing.T) { + psc := newProxySiteClient(t, createOperatingSystemMethod, nil, nil) + f.bindProxyClient(psc) + + createReq := model.APIOperatingSystemCreateRequest{ + Name: "tmpl-proxy-os", + Description: cutil.GetPtr("templated via proxy"), + IpxeTemplateId: cutil.GetPtr(f.tmpl.ID.String()), + SiteIDs: []string{f.site.ID.String()}, + IpxeTemplateParameters: model.APIOperatingSystemIpxeParameters{ + {Name: "version", Value: "22.04"}, + }, + } + body, merr := json.Marshal(createReq) + require.NoError(t, merr) + + ec, rec := f.newEchoContext(http.MethodPost, string(body), map[string]string{"orgName": f.tnOrg}) + h := CreateOperatingSystemHandler{dbSession: f.dbSession, tc: f.tc, scp: f.scp, cfg: f.cfg} + require.NoError(t, h.Handle(ec)) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + psc.client.AssertExpectations(t) + psc.workflow.AssertExpectations(t) + assert.Equal(t, createOperatingSystemMethod, psc.captured.FullMethod) + assert.Empty(t, psc.captured.EncryptedSecrets) + assertProxyCreatePayload(t, f.tnOrg, f.tmpl.ID.String(), psc.captured.RequestJSON, "22.04") + + rsp := &model.APIOperatingSystem{} + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), rsp)) + require.NotNil(t, rsp.Type) + assert.Equal(t, cdbm.OperatingSystemTypeTemplatedIPXE, *rsp.Type) + assert.Equal(t, cdbm.OperatingSystemStatusReady, rsp.Status) + require.Len(t, rsp.SiteAssociations, 1) + require.NotNil(t, rsp.SiteAssociations[0].Site) + assert.Equal(t, f.site.ID.String(), rsp.SiteAssociations[0].Site.ID) + assert.Equal(t, cdbm.OperatingSystemSiteAssociationStatusSynced, rsp.SiteAssociations[0].Status) + + osID = rsp.ID + require.NotEmpty(t, osID) + }) + + t.Run("update Templated iPXE re-syncs to sites via proxy", func(t *testing.T) { + require.NotEmpty(t, osID, "create subtest must run first") + + psc := newProxySiteClient(t, updateOperatingSystemMethod, nil, nil) + f.bindProxyClient(psc) + + updateReq := model.APIOperatingSystemUpdateRequest{ + Description: cutil.GetPtr("templated via proxy - updated"), + IpxeTemplateParameters: &model.APIOperatingSystemIpxeParameters{ + {Name: "version", Value: "24.04"}, + }, + } + body, merr := json.Marshal(updateReq) + require.NoError(t, merr) + + ec, rec := f.newEchoContext(http.MethodPatch, string(body), map[string]string{"orgName": f.tnOrg, "id": osID}) + h := UpdateOperatingSystemHandler{dbSession: f.dbSession, tc: f.tc, scp: f.scp, cfg: f.cfg} + require.NoError(t, h.Handle(ec)) + require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String()) + + psc.client.AssertExpectations(t) + psc.workflow.AssertExpectations(t) + assert.Equal(t, updateOperatingSystemMethod, psc.captured.FullMethod) + assertProxyUpdatePayload(t, osID, f.tmpl.ID.String(), psc.captured.RequestJSON, "24.04") + + rsp := &model.APIOperatingSystem{} + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), rsp)) + require.NotNil(t, rsp.Description) + assert.Equal(t, "templated via proxy - updated", *rsp.Description) + assert.Equal(t, cdbm.OperatingSystemStatusReady, rsp.Status) + require.Len(t, rsp.SiteAssociations, 1) + assert.Equal(t, cdbm.OperatingSystemSiteAssociationStatusSynced, rsp.SiteAssociations[0].Status) + }) + + t.Run("delete Templated iPXE pushes delete via proxy and soft-deletes OS", func(t *testing.T) { + require.NotEmpty(t, osID, "create subtest must run first") + + psc := newProxySiteClient(t, deleteOperatingSystemMethod, nil, nil) + f.bindProxyClient(psc) + + ec, rec := f.newEchoContext(http.MethodDelete, "", map[string]string{"orgName": f.tnOrg, "id": osID}) + h := DeleteOperatingSystemHandler{dbSession: f.dbSession, tc: f.tc, scp: f.scp, cfg: f.cfg} + require.NoError(t, h.Handle(ec)) + require.Equal(t, http.StatusAccepted, rec.Code, "body: %s", rec.Body.String()) + + psc.client.AssertExpectations(t) + psc.workflow.AssertExpectations(t) + assert.Equal(t, deleteOperatingSystemMethod, psc.captured.FullMethod) + assertProxyDeletePayload(t, osID, psc.captured.RequestJSON) + + parsedID, perr := uuid.Parse(osID) + require.NoError(t, perr) + osDAO := cdbm.NewOperatingSystemDAO(f.dbSession) + _, gerr := osDAO.GetByID(f.ctx, nil, parsedID, nil) + assert.ErrorIs(t, gerr, cdb.ErrDoesNotExist, "OS should be soft-deleted once every site is cleaned up") + }) +} + +func TestOperatingSystemHandler_TemplatedIPXE_ProxyCreateExecuteError(t *testing.T) { + f := buildTemplatedProxyFixture(t) + + psc := newProxySiteClient(t, createOperatingSystemMethod, nil, errors.New("workflow start failed")) + f.bindProxyClient(psc) + + createReq := model.APIOperatingSystemCreateRequest{ + Name: "tmpl-proxy-os-error", + IpxeTemplateId: cutil.GetPtr(f.tmpl.ID.String()), + SiteIDs: []string{f.site.ID.String()}, + } + body, err := json.Marshal(createReq) + require.NoError(t, err) + + ec, rec := f.newEchoContext(http.MethodPost, string(body), map[string]string{"orgName": f.tnOrg}) + h := CreateOperatingSystemHandler{dbSession: f.dbSession, tc: f.tc, scp: f.scp, cfg: f.cfg} + require.NoError(t, h.Handle(ec)) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + psc.client.AssertExpectations(t) + + rsp := &model.APIOperatingSystem{} + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), rsp)) + assert.Equal(t, cdbm.OperatingSystemStatusError, rsp.Status) + require.Len(t, rsp.SiteAssociations, 1) + assert.Equal(t, cdbm.OperatingSystemSiteAssociationStatusError, rsp.SiteAssociations[0].Status) + + parsedID, perr := uuid.Parse(rsp.ID) + require.NoError(t, perr) + assert.Equal(t, cdbm.OperatingSystemStatusError, f.osStatus(t, parsedID)) + assert.Equal(t, cdbm.OperatingSystemSiteAssociationStatusError, f.osAssociationStatus(t, parsedID)) +} + +func TestOperatingSystemHandler_TemplatedIPXE_ProxyCreateTimeout(t *testing.T) { + f := buildTemplatedProxyFixture(t) + + timeoutErr := tp.NewTimeoutError(enums.TIMEOUT_TYPE_UNSPECIFIED, nil, nil) + psc := newProxySiteClient(t, createOperatingSystemMethod, timeoutErr, nil) + f.bindProxyClient(psc) + + createReq := model.APIOperatingSystemCreateRequest{ + Name: "tmpl-proxy-os-timeout", + IpxeTemplateId: cutil.GetPtr(f.tmpl.ID.String()), + SiteIDs: []string{f.site.ID.String()}, + } + body, err := json.Marshal(createReq) + require.NoError(t, err) + + ec, rec := f.newEchoContext(http.MethodPost, string(body), map[string]string{"orgName": f.tnOrg}) + h := CreateOperatingSystemHandler{dbSession: f.dbSession, tc: f.tc, scp: f.scp, cfg: f.cfg} + require.NoError(t, h.Handle(ec)) + require.Equal(t, http.StatusCreated, rec.Code, "body: %s", rec.Body.String()) + + psc.client.AssertExpectations(t) + psc.workflow.AssertExpectations(t) + assert.Equal(t, createOperatingSystemMethod, psc.captured.FullMethod) + + rsp := &model.APIOperatingSystem{} + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), rsp)) + assert.Equal(t, cdbm.OperatingSystemStatusError, rsp.Status) + require.Len(t, rsp.SiteAssociations, 1) + assert.Equal(t, cdbm.OperatingSystemSiteAssociationStatusError, rsp.SiteAssociations[0].Status) + + parsedID, perr := uuid.Parse(rsp.ID) + require.NoError(t, perr) + assert.Equal(t, cdbm.OperatingSystemStatusError, f.osStatus(t, parsedID)) + assert.Equal(t, cdbm.OperatingSystemSiteAssociationStatusError, f.osAssociationStatus(t, parsedID)) +} + +// TestOperatingSystemHandler_TemplatedIPXE_TemplateNotAvailableAtSite verifies that +// creating a Templated iPXE Operating System whose referenced iPXE template has no +// IpxeTemplateSiteAssociation at the target Site is rejected up front (before any +// record is persisted or pushed), since the definition could never render there. +func TestOperatingSystemHandler_TemplatedIPXE_TemplateNotAvailableAtSite(t *testing.T) { + f := buildTemplatedProxyFixture(t) + + // A template that exists but has no association at the tenant's (only) Site. + templateDAO := cdbm.NewIpxeTemplateDAO(f.dbSession) + orphanTmpl, err := templateDAO.Create(f.ctx, nil, cdbm.IpxeTemplateCreateInput{ + ID: uuid.New(), + Name: "tmpl-proxy-template-no-itsa", + Template: "#!ipxe\n", + Visibility: "Public", + }) + require.NoError(t, err) + + createReq := model.APIOperatingSystemCreateRequest{ + Name: "tmpl-proxy-os-no-itsa", + IpxeTemplateId: cutil.GetPtr(orphanTmpl.ID.String()), + SiteIDs: []string{f.site.ID.String()}, + } + body, merr := json.Marshal(createReq) + require.NoError(t, merr) + + ec, rec := f.newEchoContext(http.MethodPost, string(body), map[string]string{"orgName": f.tnOrg}) + h := CreateOperatingSystemHandler{dbSession: f.dbSession, tc: f.tc, scp: f.scp, cfg: f.cfg} + require.NoError(t, h.Handle(ec)) + require.Equal(t, http.StatusBadRequest, rec.Code, "body: %s", rec.Body.String()) + assert.Contains(t, rec.Body.String(), "not available at Site") + + // The rejected request must not have persisted an Operating System. + osDAO := cdbm.NewOperatingSystemDAO(f.dbSession) + _, tot, gerr := osDAO.GetAll(f.ctx, nil, + cdbm.OperatingSystemFilterInput{Names: []string{"tmpl-proxy-os-no-itsa"}}, + cdbp.PageInput{Limit: cutil.GetPtr(1)}, nil) + require.NoError(t, gerr) + assert.Equal(t, 0, tot) +} diff --git a/rest-api/api/pkg/api/handler/util/common/testing.go b/rest-api/api/pkg/api/handler/util/common/testing.go index 26d1af33a3..7916f1eaeb 100644 --- a/rest-api/api/pkg/api/handler/util/common/testing.go +++ b/rest-api/api/pkg/api/handler/util/common/testing.go @@ -119,6 +119,12 @@ func TestSetupSchema(t *testing.T, dbSession *cdb.Session) { // create Operating System Site Association table err = dbSession.DB.ResetModel(context.Background(), (*cdbm.OperatingSystemSiteAssociation)(nil)) assert.Nil(t, err) + // create iPXE Template table + err = dbSession.DB.ResetModel(context.Background(), (*cdbm.IpxeTemplate)(nil)) + assert.Nil(t, err) + // create iPXE Template Site Association table (must be after IpxeTemplate due to foreign key) + err = dbSession.DB.ResetModel(context.Background(), (*cdbm.IpxeTemplateSiteAssociation)(nil)) + assert.Nil(t, err) // create Machine table err = dbSession.DB.ResetModel(context.Background(), (*cdbm.Machine)(nil)) assert.Nil(t, err) diff --git a/rest-api/api/pkg/api/model/ipxetemplate.go b/rest-api/api/pkg/api/model/ipxetemplate.go new file mode 100644 index 0000000000..08a2fd2882 --- /dev/null +++ b/rest-api/api/pkg/api/model/ipxetemplate.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package model + +import ( + "time" + + cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" +) + +// APIIpxeTemplate is the data structure to capture the API representation of an iPXE template. +// +// iPXE templates are global in REST and identified by the stable UUID assigned by core +// (`ID`). Per-site availability is tracked separately and not surfaced in this payload. +type APIIpxeTemplate struct { + // ID is the stable template UUID assigned by core, identical between core and REST + ID string `json:"id"` + // Name is the globally unique template name (e.g. "ubuntu-autoinstall", "kernel-initrd") + Name string `json:"name"` + // Template is the raw iPXE script content + Template string `json:"template"` + // RequiredParams lists the parameters that must be provided to render the template + RequiredParams []string `json:"requiredParams"` + // ReservedParams lists the parameters that are reserved by the template and cannot be user-supplied + ReservedParams []string `json:"reservedParams"` + // RequiredArtifacts lists the artifact names (e.g. "kernel", "initrd") required for the template + RequiredArtifacts []string `json:"requiredArtifacts"` + // Visibility indicates the visibility of this template: "Internal" or "Public" + Visibility string `json:"visibility"` + // Created is the date and time the entity was created in this system + Created time.Time `json:"created"` + // Updated is the date and time the entity was last updated in this system + Updated time.Time `json:"updated"` +} + +// APIIpxeTemplateGetAllRequest captures optional list filters for GET /ipxe-template. +type APIIpxeTemplateGetAllRequest struct { + SiteIDs []string `query:"siteId"` +} + +// NewAPIIpxeTemplate accepts a DB layer IpxeTemplate object and returns an API layer object +func NewAPIIpxeTemplate(dbTemplate *cdbm.IpxeTemplate) *APIIpxeTemplate { + if dbTemplate == nil { + return nil + } + + requiredParams := dbTemplate.RequiredParams + if requiredParams == nil { + requiredParams = []string{} + } + + reservedParams := dbTemplate.ReservedParams + if reservedParams == nil { + reservedParams = []string{} + } + + requiredArtifacts := dbTemplate.RequiredArtifacts + if requiredArtifacts == nil { + requiredArtifacts = []string{} + } + + return &APIIpxeTemplate{ + ID: dbTemplate.ID.String(), + Name: dbTemplate.Name, + Template: dbTemplate.Template, + RequiredParams: requiredParams, + ReservedParams: reservedParams, + RequiredArtifacts: requiredArtifacts, + Visibility: dbTemplate.Visibility, + Created: dbTemplate.Created, + Updated: dbTemplate.Updated, + } +} diff --git a/rest-api/api/pkg/api/model/operatingsystem.go b/rest-api/api/pkg/api/model/operatingsystem.go index be7c3b7c4d..a6b52f91b8 100644 --- a/rest-api/api/pkg/api/model/operatingsystem.go +++ b/rest-api/api/pkg/api/model/operatingsystem.go @@ -5,6 +5,8 @@ package model import ( "errors" + "fmt" + "strings" "time" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -32,6 +34,194 @@ func IsCloudInitFromUserData(userData *string) bool { return userData != nil && *userData != "" } +// APIOperatingSystemIpxeParameter is the API representation of a single iPXE +// template name/value parameter. It is API-owned so the REST contract does not +// depend on the persistence model. +type APIOperatingSystemIpxeParameter struct { + // Name is the parameter name (used as a variable in the template). + Name string `json:"name"` + // Value is the parameter value. + Value string `json:"value"` +} + +// toDBModel converts the receiver to the persistence model. +func (p APIOperatingSystemIpxeParameter) toDBModel() cdbm.OperatingSystemIpxeParameter { + return cdbm.OperatingSystemIpxeParameter{Name: p.Name, Value: p.Value} +} + +// APIOperatingSystemIpxeParameters is a typed list of iPXE parameters carrying +// list-level validation and conversion helpers. +type APIOperatingSystemIpxeParameters []APIOperatingSystemIpxeParameter + +// Validate checks every parameter in the list. +func (ps APIOperatingSystemIpxeParameters) Validate() error { + for i, p := range ps { + if strings.TrimSpace(p.Name) == "" { + return validation.Errors{"ipxeTemplateParameters": fmt.Errorf("entry %d: name is required", i)} + } + } + return nil +} + +// ToDBModel converts the list to the persistence model, preserving nil. +func (ps APIOperatingSystemIpxeParameters) ToDBModel() []cdbm.OperatingSystemIpxeParameter { + if ps == nil { + return nil + } + out := make([]cdbm.OperatingSystemIpxeParameter, len(ps)) + for i := range ps { + out[i] = ps[i].toDBModel() + } + return out +} + +// ToDBModelPtr converts an optional (pointer) list to the pointer persistence +// model used by update inputs, preserving a nil pointer (field not provided). +func (ps *APIOperatingSystemIpxeParameters) ToDBModelPtr() *[]cdbm.OperatingSystemIpxeParameter { + if ps == nil { + return nil + } + out := ps.ToDBModel() + return &out +} + +// APIOperatingSystemIpxeArtifact is the API (request) representation of a single +// iPXE artifact (kernel, initrd, ISO, ...). AuthToken is accepted on input but is +// never echoed back: responses use APIOperatingSystemIpxeArtifactResponse, which +// has no AuthToken field. +type APIOperatingSystemIpxeArtifact struct { + // Name is the artifact name. + Name string `json:"name"` + // URL is the original URL for the artifact. + URL string `json:"url"` + // SHA is an optional SHA256 checksum. + SHA *string `json:"sha"` + // AuthType is an optional auth type (Basic or Bearer). + AuthType *string `json:"authType"` + // AuthToken is an optional auth token, only accepted on input. + AuthToken *string `json:"authToken"` + // CacheStrategy controls how the artifact is cached on-site. + CacheStrategy string `json:"cacheStrategy"` +} + +// toDBModel converts the receiver to the persistence model. +func (a APIOperatingSystemIpxeArtifact) toDBModel() cdbm.OperatingSystemIpxeArtifact { + return cdbm.OperatingSystemIpxeArtifact{ + Name: a.Name, + URL: a.URL, + SHA: a.SHA, + AuthType: a.AuthType, + AuthToken: a.AuthToken, + CacheStrategy: a.CacheStrategy, + } +} + +// APIOperatingSystemIpxeArtifacts is a typed list of iPXE artifacts carrying +// list-level validation and conversion helpers. +type APIOperatingSystemIpxeArtifacts []APIOperatingSystemIpxeArtifact + +// Validate checks every artifact in the list. +func (as APIOperatingSystemIpxeArtifacts) Validate() error { + for i, a := range as { + if strings.TrimSpace(a.Name) == "" { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d: name is required", i)} + } + if strings.TrimSpace(a.URL) == "" { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d (%s): url is required", i, a.Name)} + } + if err := validation.Validate(a.URL, is.URL); err != nil { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d (%s): url is not valid: %w", i, a.Name, err)} + } + if _, ok := validCacheStrategies[a.CacheStrategy]; !ok { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d (%s): cacheStrategy must be one of CacheAsNeeded, LocalOnly, CachedOnly, RemoteOnly", i, a.Name)} + } + if a.AuthType != nil && *a.AuthType != "" { + at := *a.AuthType + if at != cdbm.OperatingSystemAuthTypeBasic && at != cdbm.OperatingSystemAuthTypeBearer { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d (%s): authType must be Basic or Bearer", i, a.Name)} + } + if a.AuthToken == nil || *a.AuthToken == "" { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d (%s): authToken is required when authType is specified", i, a.Name)} + } + } + if a.AuthToken != nil && *a.AuthToken != "" && (a.AuthType == nil || *a.AuthType == "") { + return validation.Errors{"ipxeTemplateArtifacts": fmt.Errorf("entry %d (%s): authType must be specified when authToken is provided", i, a.Name)} + } + } + return nil +} + +// ToDBModel converts the list to the persistence model, preserving nil. +func (as APIOperatingSystemIpxeArtifacts) ToDBModel() []cdbm.OperatingSystemIpxeArtifact { + if as == nil { + return nil + } + out := make([]cdbm.OperatingSystemIpxeArtifact, len(as)) + for i := range as { + out[i] = as[i].toDBModel() + } + return out +} + +// ToDBModelPtr converts an optional (pointer) list to the pointer persistence +// model used by update inputs, preserving a nil pointer (field not provided). +func (as *APIOperatingSystemIpxeArtifacts) ToDBModelPtr() *[]cdbm.OperatingSystemIpxeArtifact { + if as == nil { + return nil + } + out := as.ToDBModel() + return &out +} + +// APIOperatingSystemIpxeArtifactResponse is the API response representation of an +// iPXE artifact. It deliberately has no AuthToken field so stored secrets cannot +// be serialized back to clients (structural redaction). +type APIOperatingSystemIpxeArtifactResponse struct { + // Name is the artifact name. + Name string `json:"name"` + // URL is the original URL for the artifact. + URL string `json:"url"` + // SHA is an optional SHA256 checksum. + SHA *string `json:"sha"` + // AuthType is an optional auth type (Basic or Bearer). + AuthType *string `json:"authType"` + // CacheStrategy controls how the artifact is cached on-site. + CacheStrategy string `json:"cacheStrategy"` +} + +// newAPIIpxeParametersFromDB converts persisted parameters to the API response +// representation, preserving nil. +func newAPIIpxeParametersFromDB(params []cdbm.OperatingSystemIpxeParameter) []APIOperatingSystemIpxeParameter { + if params == nil { + return nil + } + out := make([]APIOperatingSystemIpxeParameter, len(params)) + for i, p := range params { + out[i] = APIOperatingSystemIpxeParameter{Name: p.Name, Value: p.Value} + } + return out +} + +// newAPIIpxeArtifactResponsesFromDB converts persisted artifacts to the API +// response representation, preserving nil. AuthToken is dropped structurally: the +// response type has no such field. +func newAPIIpxeArtifactResponsesFromDB(artifacts []cdbm.OperatingSystemIpxeArtifact) []APIOperatingSystemIpxeArtifactResponse { + if artifacts == nil { + return nil + } + out := make([]APIOperatingSystemIpxeArtifactResponse, len(artifacts)) + for i, a := range artifacts { + out[i] = APIOperatingSystemIpxeArtifactResponse{ + Name: a.Name, + URL: a.URL, + SHA: a.SHA, + AuthType: a.AuthType, + CacheStrategy: a.CacheStrategy, + } + } + return out +} + // APIOperatingSystemCreateRequest is the data structure to capture user request to create a new OperatingSystem type APIOperatingSystemCreateRequest struct { // Name is the name of the OperatingSystem @@ -70,6 +260,24 @@ type APIOperatingSystemCreateRequest struct { AllowOverride bool `json:"allowOverride"` // EnableBlockStorage indicates whether the Operating System image will be stored remotely via block storage EnableBlockStorage bool `json:"enableBlockStorage"` + // IpxeTemplateId is the ID of the iPXE template to use (alternative to a raw ipxeScript) + IpxeTemplateId *string `json:"ipxeTemplateId"` + // IpxeTemplateParameters are the parameters to pass to the iPXE template + IpxeTemplateParameters APIOperatingSystemIpxeParameters `json:"ipxeTemplateParameters"` + // IpxeTemplateArtifacts are the artifacts (kernel, initrd, ISO, ...) for the iPXE OS definition + IpxeTemplateArtifacts APIOperatingSystemIpxeArtifacts `json:"ipxeTemplateArtifacts"` +} + +// GetOperatingSystemType returns the OperatingSystem type inferred from the +// create request's source fields (`IpxeScript`, `IpxeTemplateId`, or neither). +func (oscr *APIOperatingSystemCreateRequest) GetOperatingSystemType() string { + if oscr.IpxeScript != nil { + return cdbm.OperatingSystemTypeIPXE + } + if oscr.IpxeTemplateId != nil { + return cdbm.OperatingSystemTypeTemplatedIPXE + } + return cdbm.OperatingSystemTypeImage } // Validate ensure the values passed in request are acceptable @@ -88,6 +296,25 @@ func (oscr *APIOperatingSystemCreateRequest) Validate() error { return err } + if oscr.IpxeTemplateId != nil { + if strings.TrimSpace(*oscr.IpxeTemplateId) == "" { + return validation.Errors{ + "ipxeTemplateId": errors.New("must not be empty"), + } + } + if _, err := uuid.Parse(*oscr.IpxeTemplateId); err != nil { + return validation.Errors{ + "ipxeTemplateId": errors.New("must be a valid UUID"), + } + } + } + + if oscr.IpxeScript != nil && oscr.IpxeTemplateId != nil { + return validation.Errors{ + "ipxeTemplateId": errors.New("ipxeScript and ipxeTemplateId are mutually exclusive"), + } + } + // Make sure siteIds only required in case of image is OS based if oscr.IpxeScript != nil && len(oscr.SiteIDs) > 0 { return validation.Errors{ @@ -95,13 +322,13 @@ func (oscr *APIOperatingSystemCreateRequest) Validate() error { } } - if oscr.IpxeScript != nil && oscr.ImageURL != nil { + if (oscr.IpxeScript != nil || oscr.IpxeTemplateId != nil) && oscr.ImageURL != nil { return validation.Errors{ - "imageURL": errors.New("cannot be specified for iPXE based Operating Systems"), + "imageUrl": errors.New("cannot be specified for iPXE based Operating Systems"), } - } else if oscr.IpxeScript == nil && oscr.ImageURL == nil { + } else if oscr.IpxeScript == nil && oscr.IpxeTemplateId == nil && oscr.ImageURL == nil { return validation.Errors{ - validationCommonErrorField: errors.New("either imageURL or ipxeScript must be specified"), + validationCommonErrorField: errors.New("one of imageURL, ipxeScript, or ipxeTemplateId must be specified"), } } @@ -111,6 +338,28 @@ func (oscr *APIOperatingSystemCreateRequest) Validate() error { } } + // iPXE template definition fields are only valid for Templated iPXE Operating Systems. + if oscr.IpxeTemplateId == nil { + if len(oscr.IpxeTemplateParameters) > 0 { + return validation.Errors{ + "ipxeTemplateParameters": errors.New("can only be specified for Templated iPXE Operating Systems"), + } + } + if len(oscr.IpxeTemplateArtifacts) > 0 { + return validation.Errors{ + "ipxeTemplateArtifacts": errors.New("can only be specified for Templated iPXE Operating Systems"), + } + } + } + + // Templated iPXE is validated in full by validateTemplatedIpxeOS (including its + // own image-field/site-id rules), so it returns early and never falls through to + // the image checks below. Raw iPXE and Image types have no further type-specific + // pre-checks here. + if oscr.IpxeTemplateId != nil { + return oscr.validateTemplatedIpxeOS() + } + if oscr.ImageURL != nil { err = validation.ValidateStruct(oscr, validation.Field(&oscr.ImageURL, is.URL), @@ -284,6 +533,12 @@ type APIOperatingSystemUpdateRequest struct { IsActive *bool `json:"isActive"` // DeactivationNote is the deactivation note if any DeactivationNote *string `json:"deactivationNote"` + // IpxeTemplateId is the ID of the iPXE template to use (alternative to a raw ipxeScript) + IpxeTemplateId *string `json:"ipxeTemplateId"` + // IpxeTemplateParameters are the parameters to pass to the iPXE template + IpxeTemplateParameters *APIOperatingSystemIpxeParameters `json:"ipxeTemplateParameters"` + // IpxeTemplateArtifacts are the artifacts (kernel, initrd, ISO, ...) for the iPXE OS definition + IpxeTemplateArtifacts *APIOperatingSystemIpxeArtifacts `json:"ipxeTemplateArtifacts"` } // Validate ensure the values passed in request are acceptable @@ -319,9 +574,73 @@ func (osur *APIOperatingSystemUpdateRequest) Validate(existingOS *cdbm.Operating } } + // iPXE script and template are mutually exclusive in a single request. + if osur.IpxeScript != nil && osur.IpxeTemplateId != nil { + return validation.Errors{ + "ipxeTemplateId": errors.New("ipxeScript and ipxeTemplateId are mutually exclusive"), + } + } + if osur.IpxeTemplateId != nil && strings.TrimSpace(*osur.IpxeTemplateId) == "" { + return validation.Errors{ + "ipxeTemplateId": errors.New("must not be empty"), + } + } + if osur.IpxeTemplateId != nil && osur.ImageURL != nil { + return validation.Errors{ + "imageUrl": errors.New("cannot be specified for iPXE based Operating Systems"), + } + } + + // Reject cross-type field assignments based on the existing OS type and + // validate iPXE template definition fields (Templated iPXE only). + switch existingOS.Type { + case cdbm.OperatingSystemTypeImage: + if osur.IpxeTemplateId != nil { + return validation.Errors{"ipxeTemplateId": errors.New("unable to set iPXE template for image based Operating System")} + } + case cdbm.OperatingSystemTypeIPXE: + if osur.IpxeTemplateId != nil { + return validation.Errors{"ipxeTemplateId": errors.New("unable to set iPXE template for raw iPXE Operating System")} + } + case cdbm.OperatingSystemTypeTemplatedIPXE: + if osur.IpxeScript != nil { + return validation.Errors{"ipxeScript": errors.New("unable to set iPXE script for templated iPXE Operating System")} + } + if osur.ImageURL != nil { + return validation.Errors{"imageUrl": errors.New("unable to set image URL for iPXE based Operating System")} + } + if osur.IpxeTemplateId != nil { + if strings.TrimSpace(*osur.IpxeTemplateId) == "" { + return validation.Errors{"ipxeTemplateId": errors.New("must not be empty")} + } + if _, err := uuid.Parse(*osur.IpxeTemplateId); err != nil { + return validation.Errors{"ipxeTemplateId": errors.New("must be a valid UUID")} + } + } + } + if existingOS.Type == cdbm.OperatingSystemTypeTemplatedIPXE { + if osur.IpxeTemplateParameters != nil { + if verr := osur.IpxeTemplateParameters.Validate(); verr != nil { + return verr + } + } + if osur.IpxeTemplateArtifacts != nil { + if verr := osur.IpxeTemplateArtifacts.Validate(); verr != nil { + return verr + } + } + } else { + if osur.IpxeTemplateParameters != nil { + return validation.Errors{"ipxeTemplateParameters": errors.New("can only be specified for Templated iPXE Operating Systems")} + } + if osur.IpxeTemplateArtifacts != nil { + return validation.Errors{"ipxeTemplateArtifacts": errors.New("can only be specified for Templated iPXE Operating Systems")} + } + } + if osur.IpxeScript != nil && osur.ImageURL != nil { return validation.Errors{ - "imageURL": errors.New("cannot be specified for iPXE based Operating Systems"), + "imageUrl": errors.New("cannot be specified for iPXE based Operating Systems"), } } @@ -330,7 +649,7 @@ func (osur *APIOperatingSystemUpdateRequest) Validate(existingOS *cdbm.Operating // verify if os was not created as image-based, reject the update if imageURL provided if !isImageBased && osur.ImageURL != nil { return validation.Errors{ - "imageURL": errors.New("unable to set image URL for non-image based Operating System"), + "imageUrl": errors.New("unable to set image URL for non-image based Operating System"), } } else if isImageBased && osur.IpxeScript != nil { return validation.Errors{ @@ -597,6 +916,13 @@ type APIOperatingSystem struct { RootFsLabel *string `json:"rootFsLabel"` // IpxeScript is the ipxe ocript for the Operating System IpxeScript *string `json:"ipxeScript"` + // IpxeTemplateId is the ID of the iPXE template used by this Operating System + IpxeTemplateId *string `json:"ipxeTemplateId"` + // IpxeTemplateParameters are the parameters passed to the iPXE template + IpxeTemplateParameters []APIOperatingSystemIpxeParameter `json:"ipxeTemplateParameters"` + // IpxeTemplateArtifacts are the artifacts (kernel, initrd, ISO, ...) for the iPXE OS definition. + // The response artifact type has no authToken field, so stored secrets are never echoed back. + IpxeTemplateArtifacts []APIOperatingSystemIpxeArtifactResponse `json:"ipxeTemplateArtifacts"` // PhoneHomeEnabled is an attribute which is specified by user if Operating System needs to be enabled for phone home or not PhoneHomeEnabled bool `json:"phoneHomeEnabled"` // UserData is the user data for the Operating System @@ -639,6 +965,7 @@ func NewAPIOperatingSystem(dbOS *cdbm.OperatingSystem, dbsds []cdbm.StatusDetail RootFsID: dbOS.RootFsID, RootFsLabel: dbOS.RootFsLabel, IpxeScript: dbOS.IpxeScript, + IpxeTemplateId: dbOS.IpxeTemplateId, PhoneHomeEnabled: dbOS.PhoneHomeEnabled, UserData: dbOS.UserData, IsCloudInit: IsCloudInitFromUserData(dbOS.UserData), @@ -650,6 +977,10 @@ func NewAPIOperatingSystem(dbOS *cdbm.OperatingSystem, dbsds []cdbm.StatusDetail Created: dbOS.Created, Updated: dbOS.Updated, } + apiOperatingSystem.IpxeTemplateParameters = newAPIIpxeParametersFromDB(dbOS.IpxeTemplateParameters) + // The response artifact type has no AuthToken field, so stored secrets are + // dropped structurally rather than by manual redaction. + apiOperatingSystem.IpxeTemplateArtifacts = newAPIIpxeArtifactResponsesFromDB(dbOS.IpxeTemplateArtifacts) if dbOS.InfrastructureProviderID != nil { apiOperatingSystem.InfrastructureProviderID = cutil.GetPtr(dbOS.InfrastructureProviderID.String()) } @@ -699,3 +1030,131 @@ func NewAPIOperatingSystemSummary(dbos *cdbm.OperatingSystem) *APIOperatingSyste return &aos } + +// validateTemplatedIpxeOS fully validates a Templated iPXE create request: image +// fields must be absent, at least one target site must be specified (the site list +// is fixed at creation and is immutable thereafter), and the template +// parameters/artifacts must be well-formed. +func (oscr *APIOperatingSystemCreateRequest) validateTemplatedIpxeOS() error { + if err := validation.ValidateStruct(oscr, + validation.Field(&oscr.ImageSHA, validation.Nil.Error("imageSHA cannot be specified for Templated iPXE Operating Systems")), + validation.Field(&oscr.ImageAuthType, validation.Nil.Error("imageAuthType cannot be specified for Templated iPXE Operating Systems")), + validation.Field(&oscr.ImageAuthToken, validation.Nil.Error("imageAuthToken cannot be specified for Templated iPXE Operating Systems")), + validation.Field(&oscr.ImageDisk, validation.Nil.Error("imageDisk cannot be specified for Templated iPXE Operating Systems")), + validation.Field(&oscr.RootFsID, validation.Nil.Error("rootFsId cannot be specified for Templated iPXE Operating Systems")), + validation.Field(&oscr.RootFsLabel, validation.Nil.Error("rootFsLabel cannot be specified for Templated iPXE Operating Systems")), + ); err != nil { + return err + } + + if len(oscr.SiteIDs) == 0 { + return validation.Errors{"siteIds": errors.New("at least one siteId must be specified for Templated iPXE Operating Systems")} + } + for _, siteID := range oscr.SiteIDs { + if _, err := uuid.Parse(siteID); err != nil { + return validation.Errors{"siteIds": fmt.Errorf("siteId %q is not a valid UUID", siteID)} + } + } + + if err := oscr.IpxeTemplateParameters.Validate(); err != nil { + return err + } + if err := oscr.IpxeTemplateArtifacts.Validate(); err != nil { + return err + } + return nil +} + +// validCacheStrategies is the set of accepted artifact CacheStrategy string values. +// It is derived from the DB model's strategy map so the API and persistence layers +// agree on the canonical (friendly) strategy names. +var validCacheStrategies = func() map[string]struct{} { + m := make(map[string]struct{}, len(cdbm.OperatingSystemIpxeArtifactCacheStrategyToProtoMap)) + for name := range cdbm.OperatingSystemIpxeArtifactCacheStrategyToProtoMap { + m[name] = struct{}{} + } + return m +}() + +// BuildCreateOperatingSystemRequest builds the forge.Forge CreateOperatingSystem +// request proto from a persisted Operating System record. It is used by the OS +// handler to push iPXE / Templated iPXE definitions to on-site NICo Core through +// the generic Core gRPC proxy. +// +// Note: artifact authTokens are nested inside the repeated artifacts message and +// are therefore carried as-is (the proxy cannot redact nested fields). +func BuildCreateOperatingSystemRequest(os *cdbm.OperatingSystem) *corev1.CreateOperatingSystemRequest { + return &corev1.CreateOperatingSystemRequest{ + Id: &corev1.OperatingSystemId{Value: os.ID.String()}, + Name: os.Name, + Description: os.Description, + TenantOrganizationId: tenantOrganizationIDProto(os.Org), + IsActive: os.IsActive, + AllowOverride: os.AllowOverride, + PhoneHomeEnabled: os.PhoneHomeEnabled, + UserData: os.UserData, + IpxeScript: os.IpxeScript, + IpxeTemplateId: ipxeTemplateIDProto(os.IpxeTemplateId), + IpxeTemplateParameters: ipxeParametersProto(os.IpxeTemplateParameters), + IpxeTemplateArtifacts: ipxeArtifactsProto(os.IpxeTemplateArtifacts), + } +} + +// BuildUpdateOperatingSystemRequest builds the forge.Forge UpdateOperatingSystem +// request proto from a persisted Operating System record. +func BuildUpdateOperatingSystemRequest(os *cdbm.OperatingSystem) *corev1.UpdateOperatingSystemRequest { + return &corev1.UpdateOperatingSystemRequest{ + Id: &corev1.OperatingSystemId{Value: os.ID.String()}, + Name: &os.Name, + Description: os.Description, + IsActive: &os.IsActive, + AllowOverride: &os.AllowOverride, + PhoneHomeEnabled: &os.PhoneHomeEnabled, + UserData: os.UserData, + IpxeScript: os.IpxeScript, + IpxeTemplateId: ipxeTemplateIDProto(os.IpxeTemplateId), + IpxeTemplateParameters: &corev1.IpxeTemplateParameters{Items: ipxeParametersProto(os.IpxeTemplateParameters)}, + IpxeTemplateArtifacts: &corev1.IpxeTemplateArtifacts{Items: ipxeArtifactsProto(os.IpxeTemplateArtifacts)}, + IpxeTemplateDefinitionHash: os.IpxeTemplateDefinitionHash, + } +} + +// BuildDeleteOperatingSystemRequest builds the forge.Forge DeleteOperatingSystem +// request proto for a persisted Operating System record. +func BuildDeleteOperatingSystemRequest(os *cdbm.OperatingSystem) *corev1.DeleteOperatingSystemRequest { + return &corev1.DeleteOperatingSystemRequest{ + Id: &corev1.OperatingSystemId{Value: os.ID.String()}, + } +} + +// tenantOrganizationIDProto maps a persisted org string onto the optional Core +// field. Empty means provider-owned and must be omitted (Core rejects ""). +func tenantOrganizationIDProto(org string) *string { + if org == "" { + return nil + } + return &org +} + +func ipxeTemplateIDProto(id *string) *corev1.IpxeTemplateId { + if id == nil { + return nil + } + return &corev1.IpxeTemplateId{Value: *id} +} + +func ipxeParametersProto(params []cdbm.OperatingSystemIpxeParameter) []*corev1.IpxeTemplateParameter { + out := make([]*corev1.IpxeTemplateParameter, 0, len(params)) + for i := range params { + out = append(out, params[i].ToProto()) + } + return out +} + +func ipxeArtifactsProto(artifacts []cdbm.OperatingSystemIpxeArtifact) []*corev1.IpxeTemplateArtifact { + out := make([]*corev1.IpxeTemplateArtifact, 0, len(artifacts)) + for i := range artifacts { + out = append(out, artifacts[i].ToProto()) + } + return out +} diff --git a/rest-api/api/pkg/api/model/operatingsystem_templated_test.go b/rest-api/api/pkg/api/model/operatingsystem_templated_test.go new file mode 100644 index 0000000000..ed4f7278ea --- /dev/null +++ b/rest-api/api/pkg/api/model/operatingsystem_templated_test.go @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package model + +import ( + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" + cdbm "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/model" + corev1 "github.com/NVIDIA/infra-controller/rest-api/proto/core/gen/v1" +) + +func TestOperatingSystemCreateRequest_GetOperatingSystemType(t *testing.T) { + assert.Equal(t, cdbm.OperatingSystemTypeIPXE, (&APIOperatingSystemCreateRequest{IpxeScript: cutil.GetPtr("x")}).GetOperatingSystemType()) + assert.Equal(t, cdbm.OperatingSystemTypeTemplatedIPXE, (&APIOperatingSystemCreateRequest{IpxeTemplateId: cutil.GetPtr("t")}).GetOperatingSystemType()) + assert.Equal(t, cdbm.OperatingSystemTypeImage, (&APIOperatingSystemCreateRequest{ImageURL: cutil.GetPtr("http://x")}).GetOperatingSystemType()) +} + +func TestOperatingSystemCreateRequest_Validate_Templated(t *testing.T) { + tmplID := cutil.GetPtr(uuid.NewString()) + siteIDs := []string{uuid.NewString()} + tests := []struct { + desc string + obj APIOperatingSystemCreateRequest + expectErr bool + }{ + { + desc: "templated requires at least one siteId", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: tmplID}, + expectErr: true, + }, + { + desc: "templated with siteIds is ok", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: tmplID, SiteIDs: siteIDs}, + expectErr: false, + }, + { + desc: "templated with non-UUID ipxeTemplateId is rejected", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: cutil.GetPtr("not-a-uuid"), SiteIDs: siteIDs}, + expectErr: true, + }, + { + desc: "templated with non-UUID siteId is rejected", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: tmplID, SiteIDs: []string{"not-a-uuid"}}, + expectErr: true, + }, + { + desc: "templated artifact with valid cache strategy is ok", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: tmplID, SiteIDs: siteIDs, IpxeTemplateArtifacts: APIOperatingSystemIpxeArtifacts{{Name: "kernel", URL: "http://x/k", CacheStrategy: cdbm.OperatingSystemIpxeArtifactCacheStrategyCacheAsNeeded}}}, + expectErr: false, + }, + { + desc: "templated artifact with invalid cache strategy is rejected", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: tmplID, SiteIDs: siteIDs, IpxeTemplateArtifacts: APIOperatingSystemIpxeArtifacts{{Name: "kernel", URL: "http://x/k", CacheStrategy: "BOGUS"}}}, + expectErr: true, + }, + { + desc: "templated artifact missing url is rejected", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeTemplateId: tmplID, SiteIDs: siteIDs, IpxeTemplateArtifacts: APIOperatingSystemIpxeArtifacts{{Name: "kernel", CacheStrategy: cdbm.OperatingSystemIpxeArtifactCacheStrategyCacheAsNeeded}}}, + expectErr: true, + }, + { + desc: "raw ipxe rejects template parameters", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeScript: cutil.GetPtr("ipxe"), IpxeTemplateParameters: APIOperatingSystemIpxeParameters{{Name: "p", Value: "v"}}}, + expectErr: true, + }, + { + desc: "raw ipxe rejects siteIds", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeScript: cutil.GetPtr("ipxe"), SiteIDs: siteIDs}, + expectErr: true, + }, + { + desc: "raw ipxe without siteIds is ok", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeScript: cutil.GetPtr("ipxe")}, + expectErr: false, + }, + { + desc: "ipxeScript and ipxeTemplateId mutually exclusive", + obj: APIOperatingSystemCreateRequest{Name: "abc", IpxeScript: cutil.GetPtr("ipxe"), IpxeTemplateId: tmplID}, + expectErr: true, + }, + { + desc: "image with siteId is ok", + obj: APIOperatingSystemCreateRequest{Name: "abc", ImageURL: cutil.GetPtr("http://iso.net/iso"), ImageSHA: cutil.GetPtr("a1efca12ea51069abb123bf9c77889fcc2a31cc5483fc14d115e44fdf07c7980"), RootFsID: cutil.GetPtr("666c2eee-193d-42db-a490-4c444342bd4e"), SiteIDs: siteIDs}, + expectErr: false, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + err := tc.obj.Validate() + assert.Equal(t, tc.expectErr, err != nil) + }) + } +} + +func TestOperatingSystemUpdateRequest_Validate_Template(t *testing.T) { + templatedOS := &cdbm.OperatingSystem{ID: uuid.New(), Name: "ab", Type: cdbm.OperatingSystemTypeTemplatedIPXE, Status: cdbm.OperatingSystemStatusReady} + rawIpxeOS := &cdbm.OperatingSystem{ID: uuid.New(), Name: "ab", Type: cdbm.OperatingSystemTypeIPXE, IpxeScript: cutil.GetPtr("x"), Status: cdbm.OperatingSystemStatusReady} + + t.Run("templated accepts template params", func(t *testing.T) { + err := (&APIOperatingSystemUpdateRequest{IpxeTemplateParameters: &APIOperatingSystemIpxeParameters{{Name: "p", Value: "v"}}}).Validate(templatedOS) + assert.NoError(t, err) + }) + t.Run("raw ipxe rejects template params", func(t *testing.T) { + err := (&APIOperatingSystemUpdateRequest{IpxeTemplateParameters: &APIOperatingSystemIpxeParameters{{Name: "p", Value: "v"}}}).Validate(rawIpxeOS) + assert.Error(t, err) + }) + t.Run("raw ipxe rejects template id", func(t *testing.T) { + err := (&APIOperatingSystemUpdateRequest{IpxeTemplateId: cutil.GetPtr("t")}).Validate(rawIpxeOS) + assert.Error(t, err) + }) + t.Run("templated accepts valid UUID template id", func(t *testing.T) { + err := (&APIOperatingSystemUpdateRequest{IpxeTemplateId: cutil.GetPtr(uuid.NewString())}).Validate(templatedOS) + assert.NoError(t, err) + }) + t.Run("templated rejects non-UUID template id", func(t *testing.T) { + err := (&APIOperatingSystemUpdateRequest{IpxeTemplateId: cutil.GetPtr("not-a-uuid")}).Validate(templatedOS) + assert.Error(t, err) + }) +} + +func TestBuildOperatingSystemRequests(t *testing.T) { + id := uuid.New() + authToken := "secret-token" + os := &cdbm.OperatingSystem{ + ID: id, + Name: "templated-os", + Description: cutil.GetPtr("desc"), + Org: "org-1", + Type: cdbm.OperatingSystemTypeTemplatedIPXE, + IsActive: true, + AllowOverride: true, + PhoneHomeEnabled: true, + UserData: cutil.GetPtr("ud"), + IpxeTemplateId: cutil.GetPtr("tmpl-1"), + IpxeTemplateParameters: []cdbm.OperatingSystemIpxeParameter{ + {Name: "version", Value: "22.04"}, + }, + IpxeTemplateArtifacts: []cdbm.OperatingSystemIpxeArtifact{ + {Name: "kernel", URL: "http://x/k", AuthType: cutil.GetPtr(cdbm.OperatingSystemAuthTypeBearer), AuthToken: &authToken, CacheStrategy: cdbm.OperatingSystemIpxeArtifactCacheStrategyCacheAsNeeded}, + }, + IpxeTemplateDefinitionHash: cutil.GetPtr("hash-1"), + } + + t.Run("create request maps all fields", func(t *testing.T) { + req := BuildCreateOperatingSystemRequest(os) + require.NotNil(t, req) + assert.Equal(t, id.String(), req.GetId().GetValue()) + assert.Equal(t, "templated-os", req.Name) + assert.Equal(t, "org-1", req.GetTenantOrganizationId()) + assert.True(t, req.IsActive) + assert.True(t, req.AllowOverride) + assert.True(t, req.PhoneHomeEnabled) + assert.Equal(t, "tmpl-1", req.GetIpxeTemplateId().GetValue()) + require.Len(t, req.IpxeTemplateParameters, 1) + assert.Equal(t, "version", req.IpxeTemplateParameters[0].Name) + require.Len(t, req.IpxeTemplateArtifacts, 1) + assert.Equal(t, "kernel", req.IpxeTemplateArtifacts[0].Name) + // Cache strategy maps from the friendly name to the proto enum. + assert.Equal(t, corev1.IpxeTemplateArtifactCacheStrategy_CACHE_AS_NEEDED, req.IpxeTemplateArtifacts[0].CacheStrategy) + // CachedUrl is never emitted from the rest side. + assert.Nil(t, req.IpxeTemplateArtifacts[0].CachedUrl) + }) + + t.Run("update request maps all fields", func(t *testing.T) { + req := BuildUpdateOperatingSystemRequest(os) + require.NotNil(t, req) + assert.Equal(t, id.String(), req.GetId().GetValue()) + require.NotNil(t, req.Name) + assert.Equal(t, "templated-os", *req.Name) + assert.Equal(t, "tmpl-1", req.GetIpxeTemplateId().GetValue()) + require.NotNil(t, req.IpxeTemplateParameters) + require.Len(t, req.IpxeTemplateParameters.Items, 1) + require.NotNil(t, req.IpxeTemplateArtifacts) + require.Len(t, req.IpxeTemplateArtifacts.Items, 1) + require.NotNil(t, req.IpxeTemplateDefinitionHash) + assert.Equal(t, "hash-1", *req.IpxeTemplateDefinitionHash) + }) + + t.Run("delete request maps id", func(t *testing.T) { + req := BuildDeleteOperatingSystemRequest(os) + require.NotNil(t, req) + assert.Equal(t, id.String(), req.GetId().GetValue()) + }) +} + +func TestNewAPIOperatingSystem_RedactsArtifactAuthToken(t *testing.T) { + authToken := "super-secret" + dbOS := &cdbm.OperatingSystem{ + ID: uuid.New(), + Name: "templated", + Org: "org-1", + Type: cdbm.OperatingSystemTypeTemplatedIPXE, + IpxeTemplateId: cutil.GetPtr("tmpl-1"), + IpxeTemplateArtifacts: []cdbm.OperatingSystemIpxeArtifact{ + {Name: "kernel", URL: "http://x/k", AuthType: cutil.GetPtr(cdbm.OperatingSystemAuthTypeBearer), AuthToken: &authToken, CacheStrategy: cdbm.OperatingSystemIpxeArtifactCacheStrategyCacheAsNeeded}, + }, + } + + api := NewAPIOperatingSystem(dbOS, nil, nil, nil) + require.NotNil(t, api) + require.Len(t, api.IpxeTemplateArtifacts, 1) + assert.Equal(t, "kernel", api.IpxeTemplateArtifacts[0].Name) + + // The response artifact type has no AuthToken field, so the serialized + // response can never carry the stored secret (structural redaction). + blob, err := json.Marshal(api) + require.NoError(t, err) + assert.NotContains(t, string(blob), "authToken", "serialized response must not contain an authToken field") + assert.NotContains(t, string(blob), "super-secret", "serialized response must not contain the stored secret") + + // The source DB object must not be mutated when building the response. + require.NotNil(t, dbOS.IpxeTemplateArtifacts[0].AuthToken) + assert.Equal(t, "super-secret", *dbOS.IpxeTemplateArtifacts[0].AuthToken) +} diff --git a/rest-api/api/pkg/api/routes.go b/rest-api/api/pkg/api/routes.go index 1c499eaa2a..78c6f07e9e 100644 --- a/rest-api/api/pkg/api/routes.go +++ b/rest-api/api/pkg/api/routes.go @@ -723,6 +723,17 @@ func NewAPIRoutes(dbSession *cdb.Session, tc tClient.Client, tnc tClient.Namespa Method: http.MethodDelete, Handler: apiHandler.NewDeleteOperatingSystemHandler(dbSession, tc, scp, cfg), }, + // iPXE Template endpoints (read-only; templates are synced from nico-core) + { + Path: apiPathPrefix + "/ipxe-template", + Method: http.MethodGet, + Handler: apiHandler.NewGetAllIpxeTemplateHandler(dbSession, tc, cfg), + }, + { + Path: apiPathPrefix + "/ipxe-template/:id", + Method: http.MethodGet, + Handler: apiHandler.NewGetIpxeTemplateHandler(dbSession, tc, cfg), + }, // NetworkSecurityGroup endpoints { Path: apiPathPrefix + "/network-security-group", diff --git a/rest-api/api/pkg/api/routes_test.go b/rest-api/api/pkg/api/routes_test.go index c6de7acad0..16bc471240 100644 --- a/rest-api/api/pkg/api/routes_test.go +++ b/rest-api/api/pkg/api/routes_test.go @@ -62,6 +62,7 @@ func TestNewAPIRoutes(t *testing.T) { "machine-instance-type": 3, "user": 1, "operating-system": 5, + "ipxe-template": 2, "sshkey": 5, "sshkeygroup": 5, "machine-capability": 1, @@ -127,6 +128,10 @@ func TestNewAPIRoutes(t *testing.T) { assertRouteExists(t, got, http.MethodPost, expectedMachineBatchPath) assertRouteExists(t, got, http.MethodPatch, expectedMachineBatchPath) assertRouteBefore(t, got, http.MethodPatch, expectedMachineBatchPath, http.MethodPatch, "/org/:orgName/"+cfg.GetAPIName()+"/expected-machine/:id") + + ipxeTemplatePath := "/org/:orgName/" + cfg.GetAPIName() + "/ipxe-template" + assertRouteExists(t, got, http.MethodGet, ipxeTemplatePath) + assertRouteExists(t, got, http.MethodGet, ipxeTemplatePath+"/:id") }) } } diff --git a/rest-api/db/pkg/db/model/operatingsystem.go b/rest-api/db/pkg/db/model/operatingsystem.go index b2e0fdf849..997fbad607 100644 --- a/rest-api/db/pkg/db/model/operatingsystem.go +++ b/rest-api/db/pkg/db/model/operatingsystem.go @@ -44,13 +44,6 @@ const ( // OperatingSystemTypeImage is the image based OperatingSystem type OperatingSystemTypeImage = "Image" - // OperatingSystemScopeLocal means single site, bidirectional sync (provider-owned OS from nico-core). - OperatingSystemScopeLocal = "Local" - // OperatingSystemScopeLimited means carbide-rest is the source of truth for a fixed list of sites. - OperatingSystemScopeLimited = "Limited" - // OperatingSystemScopeGlobal means carbide-rest is the source of truth for all owner sites. - OperatingSystemScopeGlobal = "Global" - // OperatingSystemOrderByDefault default field to be used for ordering when none specified OperatingSystemOrderByDefault = "created" @@ -236,22 +229,17 @@ type OperatingSystem struct { IpxeTemplateParameters []OperatingSystemIpxeParameter `bun:"ipxe_template_parameters,type:jsonb"` IpxeTemplateArtifacts []OperatingSystemIpxeArtifact `bun:"ipxe_template_artifacts,type:jsonb"` IpxeTemplateDefinitionHash *string `bun:"ipxe_template_definition_hash"` - // IpxeOsScope controls synchronization direction between carbide-rest and nico-core for - // iPXE OS definitions: "Local" is bidirectional/provider-owned from nico-core, while - // "Global" and "Limited" make carbide-rest the source of truth. nil for Image-type OS; - // legacy iPXE rows with nil scope are treated as "Local". - IpxeOsScope *string `bun:"ipxe_os_scope"` - UserData *string `bun:"user_data"` - AllowOverride bool `bun:"allow_override,notnull"` - EnableBlockStorage bool `bun:"enable_block_storage,notnull"` - PhoneHomeEnabled bool `bun:"phone_home_enabled,notnull"` - IsActive bool `bun:"is_active,notnull"` - DeactivationNote *string `bun:"deactivation_note"` // Note for deactivation, if any - Status string `bun:"status,notnull"` - Created time.Time `bun:"created,nullzero,notnull,default:current_timestamp"` - Updated time.Time `bun:"updated,nullzero,notnull,default:current_timestamp"` - Deleted *time.Time `bun:"deleted,soft_delete"` - CreatedBy uuid.UUID `bun:"type:uuid,notnull"` + UserData *string `bun:"user_data"` + AllowOverride bool `bun:"allow_override,notnull"` + EnableBlockStorage bool `bun:"enable_block_storage,notnull"` + PhoneHomeEnabled bool `bun:"phone_home_enabled,notnull"` + IsActive bool `bun:"is_active,notnull"` + DeactivationNote *string `bun:"deactivation_note"` // Note for deactivation, if any + Status string `bun:"status,notnull"` + Created time.Time `bun:"created,nullzero,notnull,default:current_timestamp"` + Updated time.Time `bun:"updated,nullzero,notnull,default:current_timestamp"` + Deleted *time.Time `bun:"deleted,soft_delete"` + CreatedBy uuid.UUID `bun:"type:uuid,notnull"` } // GetSiteID returns the OperatingSystem ID to use when communicating @@ -330,7 +318,6 @@ type OperatingSystemCreateInput struct { IpxeTemplateParameters []OperatingSystemIpxeParameter IpxeTemplateArtifacts []OperatingSystemIpxeArtifact IpxeOSHash *string - IpxeOsScope *string UserData *string AllowOverride bool EnableBlockStorage bool @@ -344,8 +331,8 @@ type OperatingSystemCreateInput struct { // template reference, template parameters, artifacts and definition hash. // // Ownership and sync-context fields (ID, Org, InfrastructureProviderID, TenantID, -// IpxeOsScope, CreatedBy and the image-* fields) are not carried on this proto and -// must be set by the caller after calling FromProto. A nil proto is a no-op. +// CreatedBy and the image-* fields) are not carried on this proto and must be set +// by the caller after calling FromProto. A nil proto is a no-op. func (in *OperatingSystemCreateInput) FromProto(protoOS *corev1.OperatingSystem) { if protoOS == nil { return @@ -412,7 +399,6 @@ type OperatingSystemUpdateInput struct { IpxeTemplateParameters *[]OperatingSystemIpxeParameter IpxeTemplateArtifacts *[]OperatingSystemIpxeArtifact IpxeOSHash *string - Scope *string UserData *string AllowOverride *bool EnableBlockStorage *bool @@ -445,7 +431,6 @@ type OperatingSystemClearInput struct { IpxeTemplateParameters bool IpxeTemplateArtifacts bool IpxeOSHash bool - Scope bool } type OperatingSystemFilterInput struct { @@ -459,8 +444,6 @@ type OperatingSystemFilterInput struct { SearchQuery *string OperatingSystemIds []uuid.UUID IsActive *bool - // Scopes filters iPXE OS definitions by their scope (e.g. "Global", "Limited", "Local"). - Scopes []string // IncludeDeleted includes soft-deleted records (used by inventory sync to detect deletions). IncludeDeleted bool } @@ -559,7 +542,6 @@ func (ossd OperatingSystemSQLDAO) Create(ctx context.Context, tx *db.Tx, input O IpxeTemplateParameters: input.IpxeTemplateParameters, IpxeTemplateArtifacts: input.IpxeTemplateArtifacts, IpxeTemplateDefinitionHash: input.IpxeOSHash, - IpxeOsScope: input.IpxeOsScope, } _, err := db.GetIDB(tx, ossd.dbSession).NewInsert().Model(os).Exec(ctx) @@ -675,17 +657,6 @@ func (ossd OperatingSystemSQLDAO) GetAll(ctx context.Context, tx *db.Tx, filter query = query.Where("os.is_active = ?", *filter.IsActive) ossd.tracerSpan.SetAttribute(operatingSystemSQLDAOSpan, "is_active", *filter.IsActive) } - if filter.Scopes != nil { - // Scope only applies to iPXE OS rows; restrict the match to iPXE types so Image rows - // (which have a NULL scope) are not coerced to "Local" by the COALESCE. - query = query.Where( - "os.type IN (?) AND COALESCE(os.ipxe_os_scope, ?) IN (?)", - bun.In([]string{OperatingSystemTypeIPXE, OperatingSystemTypeTemplatedIPXE}), - OperatingSystemScopeLocal, - bun.In(filter.Scopes), - ) - ossd.tracerSpan.SetAttribute(operatingSystemSQLDAOSpan, "scopes", filter.Scopes) - } if filter.IncludeDeleted { query = query.WhereAllWithDeleted() } @@ -861,10 +832,6 @@ func (ossd OperatingSystemSQLDAO) Update(ctx context.Context, tx *db.Tx, input O it.IpxeTemplateDefinitionHash = input.IpxeOSHash updatedFields = append(updatedFields, "ipxe_template_definition_hash") } - if input.Scope != nil { - it.IpxeOsScope = input.Scope - updatedFields = append(updatedFields, "ipxe_os_scope") - } if len(updatedFields) > 0 { updatedFields = append(updatedFields, "updated") @@ -977,10 +944,6 @@ func (ossd OperatingSystemSQLDAO) Clear(ctx context.Context, tx *db.Tx, input Op it.IpxeTemplateDefinitionHash = nil updatedFields = append(updatedFields, "ipxe_template_definition_hash") } - if input.Scope { - it.IpxeOsScope = nil - updatedFields = append(updatedFields, "ipxe_os_scope") - } if len(updatedFields) > 0 { updatedFields = append(updatedFields, "updated") diff --git a/rest-api/db/pkg/db/model/operatingsystem_ipxe_test.go b/rest-api/db/pkg/db/model/operatingsystem_ipxe_test.go index 18b2918659..e9680717e6 100644 --- a/rest-api/db/pkg/db/model/operatingsystem_ipxe_test.go +++ b/rest-api/db/pkg/db/model/operatingsystem_ipxe_test.go @@ -8,15 +8,14 @@ import ( "testing" cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" - "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db/paginator" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestOperatingSystemSQLDAO_TemplatedIPXERoundTrip exercises the iPXE template definition -// columns and the ipxe_os_scope column added for the Templated iPXE OS variant: create, -// read-back of the JSONB parameter/artifact slices, scope update, and scope/iPXE clear. +// columns added for the Templated iPXE OS variant: create, read-back of the JSONB +// parameter/artifact slices, artifact update, and iPXE definition clear. func TestOperatingSystemSQLDAO_TemplatedIPXERoundTrip(t *testing.T) { ctx := context.Background() dbSession := testOperatingSystemInitDB(t) @@ -46,10 +45,9 @@ func TestOperatingSystemSQLDAO_TemplatedIPXERoundTrip(t *testing.T) { CacheStrategy: OperatingSystemIpxeArtifactCacheStrategyCacheAsNeeded, }, }, - IpxeOSHash: cutil.GetPtr("hash-1"), - IpxeOsScope: cutil.GetPtr(OperatingSystemScopeGlobal), - Status: OperatingSystemStatusPending, - CreatedBy: user.ID, + IpxeOSHash: cutil.GetPtr("hash-1"), + Status: OperatingSystemStatusPending, + CreatedBy: user.ID, }) require.NoError(t, err) require.NotNil(t, created) @@ -57,8 +55,6 @@ func TestOperatingSystemSQLDAO_TemplatedIPXERoundTrip(t *testing.T) { got, err := dao.GetByID(ctx, nil, created.ID, nil) require.NoError(t, err) assert.Equal(t, OperatingSystemTypeTemplatedIPXE, got.Type) - require.NotNil(t, got.IpxeOsScope) - assert.Equal(t, OperatingSystemScopeGlobal, *got.IpxeOsScope) require.NotNil(t, got.IpxeTemplateId) assert.Equal(t, templateID, *got.IpxeTemplateId) require.NotNil(t, got.IpxeTemplateDefinitionHash) @@ -74,78 +70,29 @@ func TestOperatingSystemSQLDAO_TemplatedIPXERoundTrip(t *testing.T) { assert.Equal(t, "secret-token", *got.IpxeTemplateArtifacts[0].AuthToken) assert.Equal(t, OperatingSystemIpxeArtifactCacheStrategyCacheAsNeeded, got.IpxeTemplateArtifacts[0].CacheStrategy) - // Update scope and artifacts. + // Update artifacts. updated, err := dao.Update(ctx, nil, OperatingSystemUpdateInput{ OperatingSystemId: created.ID, - Scope: cutil.GetPtr(OperatingSystemScopeLimited), IpxeTemplateArtifacts: &[]OperatingSystemIpxeArtifact{ {Name: "initrd", URL: "https://example.test/initrd", CacheStrategy: OperatingSystemIpxeArtifactCacheStrategyCachedOnly}, }, }) require.NoError(t, err) - require.NotNil(t, updated.IpxeOsScope) - assert.Equal(t, OperatingSystemScopeLimited, *updated.IpxeOsScope) require.Len(t, updated.IpxeTemplateArtifacts, 1) assert.Equal(t, "initrd", updated.IpxeTemplateArtifacts[0].Name) assert.Equal(t, OperatingSystemIpxeArtifactCacheStrategyCachedOnly, updated.IpxeTemplateArtifacts[0].CacheStrategy) - // Clear the iPXE definition and scope. + // Clear the iPXE definition. cleared, err := dao.Clear(ctx, nil, OperatingSystemClearInput{ OperatingSystemId: created.ID, IpxeTemplateId: true, IpxeTemplateParameters: true, IpxeTemplateArtifacts: true, IpxeOSHash: true, - Scope: true, }) require.NoError(t, err) - assert.Nil(t, cleared.IpxeOsScope) assert.Nil(t, cleared.IpxeTemplateId) assert.Nil(t, cleared.IpxeTemplateParameters) assert.Nil(t, cleared.IpxeTemplateArtifacts) assert.Nil(t, cleared.IpxeTemplateDefinitionHash) } - -// TestOperatingSystemSQLDAO_ScopeFilter verifies the Scopes filter only matches iPXE OS rows -// and never coerces Image rows (NULL scope) into the "Local" bucket. -func TestOperatingSystemSQLDAO_ScopeFilter(t *testing.T) { - ctx := context.Background() - dbSession := testOperatingSystemInitDB(t) - defer dbSession.Close() - testOperatingSystemSetupSchema(t, dbSession) - - ip := testOperatingSystemBuildInfrastructureProvider(t, dbSession, "testIP") - tenant := testOperatingSystemBuildTenant(t, dbSession, "testTenant") - user := testOperatingSystemBuildUser(t, dbSession, "testUser") - - dao := NewOperatingSystemDAO(dbSession) - - // Global-scoped Templated iPXE OS. - _, err := dao.Create(ctx, nil, OperatingSystemCreateInput{ - Name: "global-ipxe", Org: "test", TenantID: &tenant.ID, - OsType: OperatingSystemTypeTemplatedIPXE, IpxeOsScope: cutil.GetPtr(OperatingSystemScopeGlobal), - Status: OperatingSystemStatusReady, CreatedBy: user.ID, - }) - require.NoError(t, err) - - // Raw iPXE OS with no explicit scope (treated as Local via COALESCE). - _, err = dao.Create(ctx, nil, OperatingSystemCreateInput{ - Name: "local-ipxe", Org: "test", TenantID: &tenant.ID, - OsType: OperatingSystemTypeIPXE, - Status: OperatingSystemStatusReady, CreatedBy: user.ID, - }) - require.NoError(t, err) - - // Image OS: scope does not apply (NULL scope) and must never match a scope filter. - _ = testBuildImageOperatingSystem(t, dbSession, "image-os", cutil.GetPtr("img"), "test", &ip.ID, &tenant.ID, nil, false, OperatingSystemStatusReady, user.ID) - - globalRows, _, err := dao.GetAll(ctx, nil, OperatingSystemFilterInput{Scopes: []string{OperatingSystemScopeGlobal}}, paginator.PageInput{}, nil) - require.NoError(t, err) - assert.Len(t, globalRows, 1) - assert.Equal(t, "global-ipxe", globalRows[0].Name) - - localRows, _, err := dao.GetAll(ctx, nil, OperatingSystemFilterInput{Scopes: []string{OperatingSystemScopeLocal}}, paginator.PageInput{}, nil) - require.NoError(t, err) - assert.Len(t, localRows, 1) - assert.Equal(t, "local-ipxe", localRows[0].Name) -} diff --git a/rest-api/db/pkg/migrations/20260717190000_drop_operating_system_ipxe_os_scope.go b/rest-api/db/pkg/migrations/20260717190000_drop_operating_system_ipxe_os_scope.go new file mode 100644 index 0000000000..d195f11ca1 --- /dev/null +++ b/rest-api/db/pkg/migrations/20260717190000_drop_operating_system_ipxe_os_scope.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/uptrace/bun" +) + +// Drops the operating_system.ipxe_os_scope column. The Local/Global/Limited scope +// concept is removed: synchronization direction for Templated iPXE OS definitions is +// now derived from the number of associated sites (a single site is bidirectional / +// core-driven, more than one makes carbide-rest the source of truth), so the stored +// scope is no longer needed. +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + tx, terr := db.BeginTx(ctx, &sql.TxOptions{}) + if terr != nil { + handlePanic(terr, "failed to begin transaction") + } + + _, err := tx.ExecContext(ctx, `ALTER TABLE operating_system DROP COLUMN IF EXISTS ipxe_os_scope`) + handleError(tx, err) + + terr = tx.Commit() + if terr != nil { + handlePanic(terr, "failed to commit transaction") + } + + fmt.Print(" [up migration] Dropped 'ipxe_os_scope' column from 'operating_system' table successfully. ") + return nil + }, func(ctx context.Context, db *bun.DB) error { + tx, terr := db.BeginTx(ctx, &sql.TxOptions{}) + if terr != nil { + handlePanic(terr, "failed to begin transaction") + } + + // Structural rollback only: the column is restored as a nullable TEXT column. + // The original per-row scope values cannot be recovered. + _, err := tx.ExecContext(ctx, `ALTER TABLE operating_system ADD COLUMN IF NOT EXISTS ipxe_os_scope TEXT NULL`) + handleError(tx, err) + + terr = tx.Commit() + if terr != nil { + handlePanic(terr, "failed to commit transaction") + } + + fmt.Print(" [down migration] Restored 'ipxe_os_scope' column on 'operating_system' table (values not restored). ") + return nil + }) +} diff --git a/rest-api/docs/index.html b/rest-api/docs/index.html index 35ba52743a..843a051fa3 100644 --- a/rest-api/docs/index.html +++ b/rest-api/docs/index.html @@ -464,7 +464,7 @@ -
Typical API Call Flow for Tenant
  • isCloudInit on create and update requests is deprecated and ignored; the response field is derived from whether userData is non-empty.
-

Retrieve all Operating Systems

Get an Operating System by ID

-

If the Operating System has infrastructureProviderId set, then org must have an Infrastructure Provider entity and its ID should match the Operating System Infrastructure Provider ID. User must have authorization role with PROVIDER_ADMIN suffix.

-

If the Operating System has tenantId set, then org must have a Tenant entity and its ID should match the Operating System Tenant ID. User must have authorization role with TENANT_ADMIN suffix.

+

Retrieve all Operating Systems

List Operating Systems visible to the caller's Tenant.

+

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix. Only Operating Systems whose tenantId matches the caller's Tenant are returned.

Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

query Parameters
siteId
string

Filter Operating Systems by Site ID. Can be specified multiple times to filter on more than one ID.

-
type
string
Enum: "Image" "iPXE"
type
string
Enum: "Image" "iPXE" "Templated iPXE"

Filter Operating Systems by Type

status
string

Filter Operating Systems by Status. Can be specified multiple times to filter on more than one status.

@@ -6614,7 +6612,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Specified if a Provider owns the Operating System

tenantId
string or null <uuid>

Specified if a Tenant owns the Operating System

-
type
string or null
Enum: "iPXE" "Image"
type
string or null
Enum: "iPXE" "Image" "Templated iPXE"

Type of the Operating System

imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved

@@ -6632,7 +6630,29 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Root filesystem label, only applicable for image-based Operating System

ipxeScript
string or null

iPXE script or URL, only applicable for iPXE-based Operating System

-
userData
string or null
ipxeTemplateId
string or null

ID of the iPXE template used, only present for Templated iPXE Operating System

+
Array of objects (OperatingSystemIpxeParameter)

Parameters passed to the iPXE template (Templated iPXE only)

+
Array
name
required
string

Parameter name (used as a variable in the template)

+
value
required
string

Parameter value

+
Array of objects (OperatingSystemIpxeArtifact)

Artifacts for the iPXE OS definition (Templated iPXE only). authToken is redacted.

+
Array
name
required
string

Artifact name

+
url
required
string

Original URL for the artifact

+
sha
string or null

Optional SHA256 checksum

+
authType
string or null

Optional auth type: Basic or Bearer

+
authToken
string or null

Optional auth token. Redacted in API responses.

+
cacheStrategy
string
Enum: "CacheAsNeeded" "LocalOnly" "CachedOnly" "RemoteOnly"

How to handle caching for this artifact

+
userData
string or null

User data for the Operating System

isCloudInit
boolean

Whether the Operating System is cloud-init based; true if there is non-empty userData, false otherwise.

@@ -6696,10 +6716,10 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Deprecated: Infrastructure Provider is now inferred from org membership.

tenantId
string or null <uuid>
Deprecated

Deprecated: Tenant is now inferred from org membership.

-
siteIds
Array of strings <uuid> [ items <uuid > ]

Specify only one Site if an Operating System is image-based; more than one Site is not supported.

-
ipxeScript
string or null

iPXE script or URL, only applicable for iPXE-based OS. Cannot be specified if imageUrl is specified

+
siteIds
Array of strings <uuid> [ items <uuid > ]

Target Sites for the Operating System. For image-based OS specify exactly one Site (more than one is not supported). For Templated iPXE OS at least one Site is required and the list is fixed at creation: it cannot be changed on update. Not applicable to raw iPXE OS.

+
ipxeScript
string or null
Deprecated

Deprecated: raw iPXE Operating Systems are superseded by Templated iPXE (ipxeTemplateId). iPXE script or URL, only applicable for iPXE-based OS. Cannot be specified if imageUrl is specified.

imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved; required for image-based OS. Cannot be specified if ipxeScript is specified

imageSha
string or null
Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

User data for the Operating System

isCloudInit
boolean
Deprecated

Deprecated and ignored: whether the Operating System is cloud-init based. Value now derived from userData.

-
allowOverride
boolean
allowOverride
boolean

Indicates if the user data can be overridden at Instance creation time

-

Responses

Array of objects (OperatingSystemIpxeParameter)

Parameters passed to the iPXE template (Templated iPXE only).

+
Array
name
required
string

Parameter name (used as a variable in the template)

+
value
required
string

Parameter value

+
Array of objects (OperatingSystemIpxeArtifact)

Artifacts (kernel, initrd, ISO, ...) for the iPXE OS definition (Templated iPXE only).

+
Array
name
required
string

Artifact name

+
url
required
string

Original URL for the artifact

+
sha
string or null

Optional SHA256 checksum

+
authType
string or null

Optional auth type: Basic or Bearer

+
authToken
string or null

Optional auth token. Redacted in API responses.

+
cacheStrategy
string
Enum: "CacheAsNeeded" "LocalOnly" "CachedOnly" "RemoteOnly"

How to handle caching for this artifact

+

Responses

Response Schema: application/json
id
string <uuid>

ID of the Operating System

@@ -6734,7 +6776,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Specified if a Provider owns the Operating System

tenantId
string or null <uuid>

Specified if a Tenant owns the Operating System

-
type
string or null
Enum: "iPXE" "Image"
type
string or null
Enum: "iPXE" "Image" "Templated iPXE"

Type of the Operating System

imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved

@@ -6752,7 +6794,29 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Root filesystem label, only applicable for image-based Operating System

ipxeScript
string or null

iPXE script or URL, only applicable for iPXE-based Operating System

-
userData
string or null
ipxeTemplateId
string or null

ID of the iPXE template used, only present for Templated iPXE Operating System

+
Array of objects (OperatingSystemIpxeParameter)

Parameters passed to the iPXE template (Templated iPXE only)

+
Array
name
required
string

Parameter name (used as a variable in the template)

+
value
required
string

Parameter value

+
Array of objects (OperatingSystemIpxeArtifact)

Artifacts for the iPXE OS definition (Templated iPXE only). authToken is redacted.

+
Array
name
required
string

Artifact name

+
url
required
string

Original URL for the artifact

+
sha
string or null

Optional SHA256 checksum

+
authType
string or null

Optional auth type: Basic or Bearer

+
authToken
string or null

Optional auth token. Redacted in API responses.

+
cacheStrategy
string
Enum: "CacheAsNeeded" "LocalOnly" "CachedOnly" "RemoteOnly"

How to handle caching for this artifact

+
userData
string or null

User data for the Operating System

isCloudInit
boolean

Whether the Operating System is cloud-init based; true if there is non-empty userData, false otherwise.

@@ -6822,7 +6886,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Specified if a Provider owns the Operating System

tenantId
string or null <uuid>

Specified if a Tenant owns the Operating System

-
type
string or null
Enum: "iPXE" "Image"
type
string or null
Enum: "iPXE" "Image" "Templated iPXE"

Type of the Operating System

imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved

@@ -6840,7 +6904,29 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Root filesystem label, only applicable for image-based Operating System

ipxeScript
string or null

iPXE script or URL, only applicable for iPXE-based Operating System

-
userData
string or null
ipxeTemplateId
string or null

ID of the iPXE template used, only present for Templated iPXE Operating System

+
Array of objects (OperatingSystemIpxeParameter)

Parameters passed to the iPXE template (Templated iPXE only)

+
Array
name
required
string

Parameter name (used as a variable in the template)

+
value
required
string

Parameter value

+
Array of objects (OperatingSystemIpxeArtifact)

Artifacts for the iPXE OS definition (Templated iPXE only). authToken is redacted.

+
Array
name
required
string

Artifact name

+
url
required
string

Original URL for the artifact

+
sha
string or null

Optional SHA256 checksum

+
authType
string or null

Optional auth type: Basic or Bearer

+
authToken
string or null

Optional auth token. Redacted in API responses.

+
cacheStrategy
string
Enum: "CacheAsNeeded" "LocalOnly" "CachedOnly" "RemoteOnly"

How to handle caching for this artifact

+
userData
string or null

User data for the Operating System

isCloudInit
boolean

Whether the Operating System is cloud-init based; true if there is non-empty userData, false otherwise.

@@ -6940,9 +7026,31 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Indicates if the user data can be overridden at Instance creation time

isActive
boolean or null

Indicates if the Operating System is active

-
deactivationNote
string or null
deactivationNote
string or null

Optional deactivation note if OS is inactive

-

Responses

Array of objects (OperatingSystemIpxeParameter)

Parameters passed to the iPXE template (Templated iPXE only).

+
Array
name
required
string

Parameter name (used as a variable in the template)

+
value
required
string

Parameter value

+
Array of objects (OperatingSystemIpxeArtifact)

Artifacts (kernel, initrd, ISO, ...) for the iPXE OS definition (Templated iPXE only).

+
Array
name
required
string

Artifact name

+
url
required
string

Original URL for the artifact

+
sha
string or null

Optional SHA256 checksum

+
authType
string or null

Optional auth type: Basic or Bearer

+
authToken
string or null

Optional auth token. Redacted in API responses.

+
cacheStrategy
string
Enum: "CacheAsNeeded" "LocalOnly" "CachedOnly" "RemoteOnly"

How to handle caching for this artifact

+

Responses

Response Schema: application/json
id
string <uuid>

ID of the Operating System

@@ -6954,7 +7062,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Specified if a Provider owns the Operating System

tenantId
string or null <uuid>

Specified if a Tenant owns the Operating System

-
type
string or null
Enum: "iPXE" "Image"
type
string or null
Enum: "iPXE" "Image" "Templated iPXE"

Type of the Operating System

imageUrl
string or null <uri>

Original URL from which the Operating System image can be retrieved

@@ -6972,7 +7080,29 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa fiNpIH bAoMjv">

Root filesystem label, only applicable for image-based Operating System

ipxeScript
string or null

iPXE script or URL, only applicable for iPXE-based Operating System

-
userData
string or null
ipxeTemplateId
string or null

ID of the iPXE template used, only present for Templated iPXE Operating System

+
Array of objects (OperatingSystemIpxeParameter)

Parameters passed to the iPXE template (Templated iPXE only)

+
Array
name
required
string

Parameter name (used as a variable in the template)

+
value
required
string

Parameter value

+
Array of objects (OperatingSystemIpxeArtifact)

Artifacts for the iPXE OS definition (Templated iPXE only). authToken is redacted.

+
Array
name
required
string

Artifact name

+
url
required
string

Original URL for the artifact

+
sha
string or null

Optional SHA256 checksum

+
authType
string or null

Optional auth type: Basic or Bearer

+
authToken
string or null

Optional auth token. Redacted in API responses.

+
cacheStrategy
string
Enum: "CacheAsNeeded" "LocalOnly" "CachedOnly" "RemoteOnly"

How to handle caching for this artifact

+
userData
string or null

User data for the Operating System

isCloudInit
boolean

Whether the Operating System is cloud-init based; true if there is non-empty userData, false otherwise.

@@ -7018,7 +7148,75 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "allowOverride": true
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "iPXE",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "allowOverride": true,
  • "phoneHomeEnabled": true,
  • "imageAuthToken": null,
  • "imageAuthType": null,
  • "imageDisk": null,
  • "imageSha": null,
  • "imageUrl": null,
  • "rootFsId": null,
  • "rootFsLabel": null,
  • "siteAssociations": [ ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/operating-system/{operatingSystemId}

Request samples

Content type
application/json
{
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "allowOverride": true
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "iPXE",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "allowOverride": true,
  • "phoneHomeEnabled": true,
  • "imageAuthToken": null,
  • "imageAuthType": null,
  • "imageDisk": null,
  • "imageSha": null,
  • "imageUrl": null,
  • "rootFsId": null,
  • "rootFsLabel": null,
  • "siteAssociations": [ ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

iPXE Template

iPXE Templates are read-only definitions propagated from nico-core into the REST API. Each template has a stable core UUID and a globally unique name; per-site availability is tracked in IpxeTemplateSiteAssociation records rather than on the template row itself. List and retrieve operations return templates reported at sites the caller is authorized for (optionally filtered by siteId). Templated iPXE Operating Systems reference a template by ipxeTemplateId.

+

Get all iPXE templates

Get all iPXE templates propagated from nico-core. Optionally restrict to one or more sites with the siteId query parameter.

+

The Infrastructure Provider and Tenant are inferred from the org's membership. User must have authorization role with PROVIDER_ADMIN, PROVIDER_VIEWER, or TENANT_ADMIN suffix.

+
Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

+
query Parameters
siteId
Array of strings <uuid> [ items <uuid > ]

Optional site ID(s); may be repeated to restrict results to templates available at any of the sites

+
pageNumber
integer >= 1
Default: 1
Example: pageNumber=1

Page number for pagination query

+
pageSize
integer [ 1 .. 100 ]
Example: pageSize=20

Page size for pagination query

+
orderBy
string
Enum: "NAME_ASC" "NAME_DESC" "CREATED_ASC" "CREATED_DESC" "UPDATED_ASC" "UPDATED_DESC"

Ordering for pagination query

+

Responses

Response Headers
X-Pagination
string
Example: "{\"pageNumber\":1,\"pageSize\":20,\"total\":30,\"orderBy\": \"NAME_ASC\"}"

Pagination result in JSON format

+
Response Schema: application/json
Array
id
required
string <uuid>

Stable template UUID assigned by core

+
name
required
string

Globally unique template name

+
template
required
string

Raw iPXE script content

+
requiredParams
required
Array of strings

Parameters that must be provided to render the template

+
reservedParams
required
Array of strings

Parameters reserved by the template and not user-supplied

+
requiredArtifacts
required
Array of strings

Artifact names required for the template

+
visibility
required
string

Template visibility: Internal or Public

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve an iPXE template

Retrieve an iPXE template by its stable core ID. The caller must be authorized for at least one Site at which the template is available.

+

The Infrastructure Provider and Tenant are inferred from the org's membership. User must have authorization role with PROVIDER_ADMIN, PROVIDER_VIEWER, or TENANT_ADMIN suffix.

+
Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

+
ipxeTemplateId
required
string <uuid>

Stable template ID (UUID from core)

+

Responses

Response Schema: application/json
id
required
string <uuid>

Stable template UUID assigned by core

+
name
required
string

Globally unique template name

+
template
required
string

Raw iPXE script content

+
requiredParams
required
Array of strings

Parameters that must be provided to render the template

+
reservedParams
required
Array of strings

Parameters reserved by the template and not user-supplied

+
requiredArtifacts
required
Array of strings

Artifact names required for the template

+
visibility
required
string

Template visibility: Internal or Public

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

Response samples

Content type
application/json
{
  • "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  • "name": "ubuntu-autoinstall",
  • "template": "#!ipxe\nkernel ${kernel_url} initrd=initrd autoinstall\ninitrd ${initrd_url}\nboot\n",
  • "requiredParams": [
    ],
  • "reservedParams": [
    ],
  • "requiredArtifacts": [
    ],
  • "visibility": "Public",
  • "created": "2026-07-14T14:15:22Z",
  • "updated": "2026-07-14T14:15:22Z"
}

Instance Type

Instance Types allow grouping Machines into a pool defined by their capabilities. Providers can then allocate a portion of the Instance Type pool to a Tenant.

Retrieve all Instance Types

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance Type

Create an Instance Type for Infrastructure Provider.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7294,7 +7492,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
Example
{
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "siteId": "8d97fa69-9199-49ff-bcf3-168c62d3874e",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type

Request samples

Content type
application/json
Example
{
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "siteId": "8d97fa69-9199-49ff-bcf3-168c62d3874e",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an Instance Type

Get an Instance Type by ID.

@@ -7412,7 +7610,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "allocationStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "allocationStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance Type

Delete an Instance Type by ID.

Org must have an Infrastructure Provider entity that owns the Instance Type. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7426,7 +7624,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance Type

Update an Instance Type by ID.

Org must have an Infrastructure Provider entity that owns the Instance Type. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7568,7 +7766,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "description": "Updated version of the X3 Large family of machines",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Updated version of the X3 Large family of machines",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Machines/Instance Type associations

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}

Request samples

Content type
application/json
{
  • "description": "Updated version of the X3 Large family of machines",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Updated version of the X3 Large family of machines",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Machines/Instance Type associations

Get all Machines for a given Instance Type

Org must have an Infrastructure Provider entity that owns the Instance Type and the Machine. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7614,7 +7812,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Create a Machine/Instance Type association

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}/machine

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Create a Machine/Instance Type association

Associate a Machine to an Instance Type

@@ -7658,7 +7856,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    }
]

Delete a Machine/Instance Type association

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}/machine

Request samples

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    }
]

Delete a Machine/Instance Type association

Delete a Machine's association with an Instance Type.

@@ -7676,7 +7874,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}/machine/{machineAssociationId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Instance

Instance is a Machine provisioned with an Operating System by a Tenant and attached to one or more VPC Prefixes or Subnets.

Retrieve all Instances

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance

Create an Instance for Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -8254,7 +8452,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
Example
{
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
Example
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [ ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Batch Create Instances

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance

Request samples

Content type
application/json
Example
{
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
Example
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [ ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Batch Create Instances

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
Example
{
  • "namePrefix": "gpu-worker",
  • "count": 4,
  • "description": "GPU worker nodes for distributed training",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "topologyOptimized": true,
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/batch

Request samples

Content type
application/json
Example
{
  • "namePrefix": "gpu-worker",
  • "count": 4,
  • "description": "GPU worker nodes for distributed training",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "topologyOptimized": true,
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Instance

Get an Instance by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -8810,7 +9008,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "networkSecurityGroupId": "c602eb90-3039-11f0-997a-b38d4fc8389e",
  • "networkSecurityGroupPropagationDetails": {
    },
  • "networkSecurityGroupInherited": false,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "networkSecurityGroupId": "c602eb90-3039-11f0-997a-b38d4fc8389e",
  • "networkSecurityGroupPropagationDetails": {
    },
  • "networkSecurityGroupInherited": false,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance

Delete an Instance by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -8836,7 +9034,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "machineHealthIssue": {
    },
  • "isRepairTenant": false
}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}

Request samples

Content type
application/json
{
  • "machineHealthIssue": {
    },
  • "isRepairTenant": false
}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance

Update an Instance by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -9152,7 +9350,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "spark-monitor-1",
  • "description": "Spark Monitor Node 1",
  • "triggerReboot": true,
  • "rebootWithCustomIpxe": true,
  • "applyUpdatesOnReboot": true,
  • "sshKeyGroupIds": [
    ],
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-2",
  • "description": "Spark Monitor Node 1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": "158fc2bc-f2fb-4e1f-a5a4-2211062d14df",
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Rebooting",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Instance status history

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}

Request samples

Content type
application/json
{
  • "name": "spark-monitor-1",
  • "description": "Spark Monitor Node 1",
  • "triggerReboot": true,
  • "rebootWithCustomIpxe": true,
  • "applyUpdatesOnReboot": true,
  • "sshKeyGroupIds": [
    ],
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-2",
  • "description": "Spark Monitor Node 1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": "158fc2bc-f2fb-4e1f-a5a4-2211062d14df",
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Rebooting",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Instance status history

Get Instance status history

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -9182,7 +9380,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Interfaces

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}/status-history

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Interfaces

Get all Interfaces for an Instance

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -9240,7 +9438,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve all Instance InfiniBand Interfaces

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}/interface

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve all Instance InfiniBand Interfaces

Get all InfiniBand Interfaces for an Instance

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -9290,7 +9488,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}/nvlink-interface

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Machine

Machine is a physical server that contains CPUs, GPUs, memory, storage, and networking hardware. Machines are the physical building blocks of a Site.

Retrieve all Machines

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Machine

Org must have either an Infrastructure Provider entity or a Tenant entity.

@@ -9756,7 +9954,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
Example
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}

Response samples

Content type
application/json
Example
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Machine

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
Example
{
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52"
}

Response samples

Content type
application/json
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a Machine from a Site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}

Request samples

Content type
application/json
Example
{
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52"
}

Response samples

Content type
application/json
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a Machine from a Site

Org must have an Infrastructure Provider entity. Machine must belong to the Provider. User must have authorization role with PROVIDER_ADMIN suffix. Machine must meet certain criteria to be eligible for deletion.

Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

@@ -10046,7 +10244,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 500 Internal Server Error

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Machine power control

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Machine power control

Execute power control actions for a specific Machine.

Org must have an Infrastructure Provider entity and own the Site that the Machine belongs to. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -10070,7 +10268,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
Example
{
  • "action": "On"
}

Response samples

Content type
application/json
{
  • "message": "Power control accepted"
}

Retrieve Machine status history

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/power

Request samples

Content type
application/json
Example
{
  • "action": "On"
}

Response samples

Content type
application/json
{
  • "message": "Power control accepted"
}

Retrieve Machine status history

Org must have either an Infrastructure Provider entity or a Tenant entity.

@@ -10102,7 +10300,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve DPU Machines attached to a host Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/status-history

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve DPU Machines attached to a host Machine

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve GPU stats for machines at a site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/dpu

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve GPU stats for machines at a site

Returns GPU summary stats grouped by GPU name for machines at the specified site.

User must have authorization role with PROVIDER_ADMIN suffix. The specified site must belong to the Provider.

@@ -10308,7 +10506,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve machine instance type assignment summary for a site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/gpu/stats

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve machine instance type assignment summary for a site

Returns machine counts grouped by assigned (has instance type) vs unassigned, broken down by status.

User must have authorization role with PROVIDER_ADMIN suffix. The specified site must belong to the Provider.

@@ -10354,7 +10552,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "assigned": {
    },
  • "unassigned": {
    }
}

Retrieve detailed per-instance-type machine stats for a site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/instance-type/stats/summary

Response samples

Content type
application/json
{
  • "assigned": {
    },
  • "unassigned": {
    }
}

Retrieve detailed per-instance-type machine stats for a site

Returns machine stats for each instance type including allocation details and tenant breakdown.

User must have authorization role with PROVIDER_ADMIN suffix. The specified site must belong to the Provider.

@@ -10422,7 +10620,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Machine Capabilities

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/instance-type/stats

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Machine Capabilities

Get all distinct Machine Capabilities across all Machines

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -10482,7 +10680,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    }
]

BMC Reset

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine-capability

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    }
]

BMC Reset

BMC Reset allows resetting a Machine's BMC

Reset Machine BMC

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
{
  • "useIpmiTool": true
}

Response samples

Content type
application/json
{
  • "message": "Machine BMC reset request was accepted"
}

DPU Reprovision

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/bmc/reset

Request samples

Content type
application/json
{
  • "useIpmiTool": true
}

Response samples

Content type
application/json
{
  • "message": "Machine BMC reset request was accepted"
}

DPU Reprovision

DPU Reprovision allows re-provisioning Machine's DPUs

Reprovision Machine DPUs

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
{
  • "mode": "Restart",
  • "updateFirmware": true
}

Response samples

Content type
application/json
{
  • "message": "DPU reprovisioning request was accepted"
}

Health Report

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/dpu/reprovision

Request samples

Content type
application/json
{
  • "mode": "Restart",
  • "updateFirmware": true
}

Response samples

Content type
application/json
{
  • "message": "DPU reprovisioning request was accepted"
}

Health Report

Machine Health Report contains information about the health of a Machine including user enforced overrides

Retrieve all Machine health reports

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Response samples

Content type
application/json
[
  • {
    }
]

Create or update Machine health report

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/health-report

Response samples

Content type
application/json
[
  • {
    }
]

Create or update Machine health report

Add or update health report override for a specific Machine.

@@ -10664,7 +10862,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "triggeredBy": "operator",
  • "observedAt": "2026-06-24T12:00:00Z",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Delete Machine health report

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/health-report

Request samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "triggeredBy": "operator",
  • "observedAt": "2026-06-24T12:00:00Z",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Delete Machine health report

Remove a health report override for a specific Machine.

@@ -10688,7 +10886,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Machine Capability

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/health-report/{source}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Machine Capability

Machine Capability defines the hardware capabilities of a Machine. Machine Capabilities can be used to group Machines into Instance Types.

Rack

Rack is a physical enclosure that contains a number of Machines. Racks are the physical building blocks of a Site.

@@ -10778,7 +10976,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Rack

Get a Rack by ID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -10856,7 +11054,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/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": [
    ]
}

Validate Racks

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Rack

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

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Rack

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

@@ -10948,7 +11146,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Racks

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

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Racks

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

@@ -10988,7 +11186,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/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.

@@ -11038,7 +11236,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/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 authorization role with PROVIDER_ADMIN suffix.

@@ -11076,7 +11274,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/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 authorization role with PROVIDER_ADMIN suffix.

@@ -11162,7 +11360,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/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 authorization role with PROVIDER_ADMIN suffix.

@@ -11198,7 +11396,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/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 authorization role with PROVIDER_ADMIN suffix.

@@ -11230,7 +11428,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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": [
    ]
}

Retrieve all Tasks for a Rack

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

Request samples

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

Response samples

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

Retrieve all Tasks for a Rack

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
[
  • {
    }
]

Tray

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

Response samples

Content type
application/json
[
  • {
    }
]

Tray

Tray represents a component within a Rack.

Retrieve all Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Tray

Get a Tray by ID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11448,7 +11646,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "660e8400-e29b-41d4-a716-446655440001",
  • "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "type": "compute",
  • "name": "compute-tray-1",
  • "manufacturer": "NVIDIA",
  • "model": "GB200",
  • "serialNumber": "TSN001",
  • "description": "Compute tray in slot 1",
  • "firmwareVersion": "2.1.0",
  • "powerState": "on",
  • "position": {
    },
  • "rackId": "550e8400-e29b-41d4-a716-446655440000"
}

Validate Trays

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}

Response samples

Content type
application/json
{
  • "id": "660e8400-e29b-41d4-a716-446655440001",
  • "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "type": "compute",
  • "name": "compute-tray-1",
  • "manufacturer": "NVIDIA",
  • "model": "GB200",
  • "serialNumber": "TSN001",
  • "description": "Compute tray in slot 1",
  • "firmwareVersion": "2.1.0",
  • "powerState": "on",
  • "position": {
    },
  • "rackId": "550e8400-e29b-41d4-a716-446655440000"
}

Validate Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/validation

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Tray

Validate a Tray by comparing expected vs actual state.

@@ -11550,7 +11748,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Trays

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

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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": [
    ]
}

Power control a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/power

Request samples

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

Response samples

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

Power control a Tray

Power control a Tray identified by Tray UUID.

@@ -11662,7 +11860,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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 Trays

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{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 Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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 Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/firmware

Request samples

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

Response samples

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

Firmware update a Tray

Update firmware on a Tray identified by Tray UUID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11860,7 +12058,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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": [
    ]
}

Retrieve all Tasks for a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{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": [
    ]
}

Retrieve all Tasks for a Tray

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
[
  • {
    }
]

Task

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}/task

Response samples

Content type
application/json
[
  • {
    }
]

Task

Task represents an asynchronous, site-scoped operation (for example firmware update, power state change, or rack bring-up). Tasks are created when operations run against Racks, Trays, or other components. Endpoints in this tag retrieve or cancel a Task by ID; list Tasks for a Rack or Tray under the Rack and Tray tags.

Retrieve a Task

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Running",
  • "description": "Power on rack components",
  • "message": "Processing 3 of 5 components"
}

Cancel a Task

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/{id}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Running",
  • "description": "Power on rack components",
  • "message": "Processing 3 of 5 components"
}

Cancel a Task

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

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

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Terminated",
  • "description": "Power on rack components",
  • "message": "Cancelled by user"
}

Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/{id}/cancel

Request samples

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

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Terminated",
  • "description": "Power on rack components",
  • "message": "Cancelled by user"
}

Rule

Operation Rule defines, per Site, how a particular operation (for example PowerControl / power_on or FirmwareControl / upgrade) should be executed against a set of components: ordered execution stages, per-component-type concurrency, pre / main / post actions, timeouts, and retry policy. Rules are reusable templates owned by Flow; this tag exposes CRUD (POST, GET, PATCH, DELETE) over them.

Create an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

List Operation Rules

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

List Operation Rules

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve an Operation Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update an Operation Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule/{id}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Delete an Operation Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule/{id}

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Delete an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule/{id}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Network Security Group

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Response samples

Content type
application/json
[
  • {
    }
]

Create Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group

Response samples

Content type
application/json
[
  • {
    }
]

Create Network Security Group

Create a Network Security Group for Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -12692,7 +12890,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Request samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "188a8f32-0001-45cf-b243-f62720a22cc4",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Retrieve Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group

Request samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "188a8f32-0001-45cf-b243-f62720a22cc4",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Retrieve Network Security Group

Get a Network Security Group by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -12778,7 +12976,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Update Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group/{networkSecurityGroupId}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Update Network Security Group

Update a Network Security Group by ID

@@ -12892,7 +13090,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Request samples

Content type
application/json
{
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Delete Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group/{networkSecurityGroupId}

Request samples

Content type
application/json
{
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Delete Network Security Group

Delete a Network Security Group by ID

@@ -12918,7 +13116,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group/{networkSecurityGroupId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

IP Block

IP Block is a contiguous block of IP addresses defined by a prefix and prefix length.

It can be used by the Provider to describe the overlay network of a particular Site. Providers can also use Allocations to delegate portions of these IP Blocks to Tenants.

@@ -13004,7 +13202,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Create IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock

Response samples

Content type
application/json
[
  • {
    }
]

Create IP Block

Create an IP block for the org.

@@ -13082,7 +13280,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve IP Block

Retrieve an IP Block by ID.

User must have authorization role with PROVIDER_ADMIN or TENANT_ADMIN suffix.

@@ -13152,7 +13350,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "192.168.20.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "usageStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "192.168.20.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "usageStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete IP Block

Delete an IP block

@@ -13170,7 +13368,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update IP Block

Update an existing IP Block

@@ -13240,7 +13438,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.16.0",
  • "prefixLength": 20,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve All Derived IP Blocks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.16.0",
  • "prefixLength": 20,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve All Derived IP Blocks

Retrieve all child IP Blocks allocated to Tenants from a specific Provider super IP Block. When allocations are created from a super block, individual Tenant IP Blocks are created as a result.

@@ -13318,7 +13516,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}/derived

Response samples

Content type
application/json
[
  • {
    }
]

DPU Extension Service

DPU Extension Service allows users to run custom services in the DPUs of their Instances. Currently K8s pods are the only supported service type.

Retrieve all DPU Extension Services

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Create DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service

Response samples

Content type
application/json
[
  • {
    }
]

Create DPU Extension Service

Create a DPU Extension Service for the current Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -13478,7 +13676,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service

Request samples

Content type
application/json
{
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service

Retrieve a DPU Extension Service for the current Tenant by ID

@@ -13538,7 +13736,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete DPU Extension Service

Delete a specific DPU Extension Service by ID. All versions will be deleted.

@@ -13554,7 +13752,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update DPU Extension Service

Update a specific DPU Extension Service.

@@ -13644,7 +13842,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "name": "busybox-ha",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 3 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service Version

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}

Request samples

Content type
application/json
{
  • "name": "busybox-ha",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 3 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service Version

Retrieve details for a specific version of a DPU Extension Service.

@@ -13680,7 +13878,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "version": "V1-T1761856992374052",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "hasCredentials": true,
  • "created": "2019-08-24T14:15:22Z",
  • "observability": {
    }
}

Delete DPU Extension Service Version

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}/version/{version}

Response samples

Content type
application/json
{
  • "version": "V1-T1761856992374052",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "hasCredentials": true,
  • "created": "2019-08-24T14:15:22Z",
  • "observability": {
    }
}

Delete DPU Extension Service Version

Delete a specific version of a DPU Extension Service.

@@ -13698,7 +13896,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}/version/{version}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

SSH Key Group

SSH Key Groups allow grouping several SSH Keys together so they can be synced to Sites and used to access the Serial Console of Instances.

Retrieve all SSH Key Groups

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key Group

Create an SSH Key Group for the current Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -13860,7 +14058,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup

Request samples

Content type
application/json
{
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH Key Group

Retrieve an SSH Key Group for the current Tenant by ID

@@ -13930,7 +14128,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup/{sshKeyGroupId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key Group

Delete a specific SSH key Group.

@@ -13946,7 +14144,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup/{sshKeyGroupId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key Group

Update a specific SSH Key Group.

@@ -14030,7 +14228,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ],
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup/{sshKeyGroupId}

Request samples

Content type
application/json
{
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ],
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

SSH Key

SSH Key is a public key that can be used to access the Serial Console of an Instance.

Retrieve all SSH Keys

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key

Create an SSH Key for the current Tenant. If an SSH Key Group is specified, all Sites associated with the SSH Key Group must be online.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -14102,7 +14300,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "reno-sre-access",
  • "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICip4hl6WjuVHs60PeikVUs0sWE/kPhk2D0rRHWsIuyL jdoe@test.com",
  • "sshKeyGroupId": "86ca8cab-b285-4c2d-9e00-25c88810dc2e"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey

Request samples

Content type
application/json
{
  • "name": "reno-sre-access",
  • "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICip4hl6WjuVHs60PeikVUs0sWE/kPhk2D0rRHWsIuyL jdoe@test.com",
  • "sshKeyGroupId": "86ca8cab-b285-4c2d-9e00-25c88810dc2e"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH key

Retrieve an SSH key for the current Tenant by ID

@@ -14130,7 +14328,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "staging-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey/{sshKeyId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "staging-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key

Delete an SSH key for the current Tenant by ID.

@@ -14146,7 +14344,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey/{sshKeyId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Request samples

Content type
application/json
{
  • "name": "reno-sre-access-v2"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access-v2",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

User

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey/{sshKeyId}

Request samples

Content type
application/json
{
  • "name": "reno-sre-access-v2"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access-v2",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

User

User is a logical entity that identifies individuals operating on behalf of an organization.

Retrieve Current User

Retrieve details of the current user.

@@ -14206,7 +14404,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authenticated

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "email": "janed@nvidia.com",
  • "firstName": "Jane",
  • "lastName": "Doe",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Audit

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/user/current

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "email": "janed@nvidia.com",
  • "firstName": "Jane",
  • "lastName": "Doe",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Audit

Audit is a record of actions taken by users on the API.

Retrieve all Audit Log Entries

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Audit Log Entry

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/audit

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Audit Log Entry

Retrieve a specific Audit Log Entry by ID

User must have authorization role with PROVIDER_ADMIN or TENANT_ADMIN suffix

@@ -14326,7 +14524,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

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

Response samples

Content type
application/json
{
  • "id": "e313b3ca-c47a-4ec1-a79b-a147fad51a50",
  • "endpoint": "/v2/org/test-org-1/nico/ep",
  • "queryParams": "{\"test\":[\"1234\"]}",
  • "method": "POST",
  • "body": "{\"key1\":\"value1\"}",
  • "statusCode": 200,
  • "clientIP": "12.123.43.112",
  • "userID": "5d9fe319-14d4-40e3-8e5a-7d79e680d55b",
  • "user": {
    },
  • "orgName": "test-org-1",
  • "timestamp": "2024-12-04T21:06:33.849293-08:00",
  • "durationMs": 250,
  • "apiVersion": "0.1.91"
}

Metadata

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/audit/{auditEntryId}

Response samples

Content type
application/json
{
  • "id": "e313b3ca-c47a-4ec1-a79b-a147fad51a50",
  • "endpoint": "/v2/org/test-org-1/nico/ep",
  • "queryParams": "{\"test\":[\"1234\"]}",
  • "method": "POST",
  • "body": "{\"key1\":\"value1\"}",
  • "statusCode": 200,
  • "clientIP": "12.123.43.112",
  • "userID": "5d9fe319-14d4-40e3-8e5a-7d79e680d55b",
  • "user": {
    },
  • "orgName": "test-org-1",
  • "timestamp": "2024-12-04T21:06:33.849293-08:00",
  • "durationMs": 250,
  • "apiVersion": "0.1.91"
}

Metadata

Metadata describes various system-level attributes of the API service.

Retrieve metadata about the API server

Retrieve system metadata providing information about the API server

@@ -14342,7 +14540,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authenticated

Response samples

Content type
application/json
{
  • "version": "0.1.24",
  • "buildTime": "2019-08-24T14:15:22Z"
}

Host Firmware Config

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/metadata

Response samples

Content type
application/json
{
  • "version": "0.1.24",
  • "buildTime": "2019-08-24T14:15:22Z"
}

Host Firmware Config

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable, so the host firmware config request cannot be served.

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ]
}

Response samples

Content type
application/json
{
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ],
  • "created": "2025-08-24T14:15:22Z",
  • "updated": "2025-08-24T14:15:22Z"
}

Delete Host Firmware Config

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/firmware-config/host

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ]
}

Response samples

Content type
application/json
{
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ],
  • "created": "2025-08-24T14:15:22Z",
  • "updated": "2025-08-24T14:15:22Z"
}

Delete Host Firmware Config

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable, so the host firmware config request cannot be served.

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100"
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Tenant Identity

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/firmware-config/host

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100"
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Tenant Identity

Typical API Call Flow for Tenant be served.

Request samples

Content type
application/json
Example
{
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [ ],
  • "tokenTtlSeconds": 1,
  • "subjectPrefix": "string",
  • "rotateKey": false
}

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Tenant Identity Configuration for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/config

Request samples

Content type
application/json
Example
{
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [ ],
  • "tokenTtlSeconds": 1,
  • "subjectPrefix": "string",
  • "rotateKey": false
}

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Tenant Identity Configuration for current Org

Typical API Call Flow for Tenant be served.

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Tenant Identity Configuration

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/config

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Tenant Identity Configuration

Typical API Call Flow for Tenant be served.

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Create or Update Token Delegation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/config

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Create or Update Token Delegation

Typical API Call Flow for Tenant be served.

Request samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string"
}

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Token Delegation for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/token-delegation

Request samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string"
}

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Token Delegation for current Org

Retrieve the registered token exchange callback for the tenant.

@@ -15040,7 +15238,7 @@

Typical API Call Flow for Tenant

be served.

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Token Delegation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/token-delegation

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Token Delegation

Delete the RFC 8693 token exchange callback for the tenant.

@@ -15068,7 +15266,7 @@

Typical API Call Flow for Tenant

be served.

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Retrieve OIDC JWKS for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/token-delegation

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Retrieve OIDC JWKS for current Org

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable.

Response samples

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

Retrieve OpenID Configuration for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/.well-known/jwks.json

Response samples

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

Retrieve OpenID Configuration for current Org

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable.

Response samples

Content type
application/json
{
  • "issuer": "string",
  • "jwks_uri": "http://example.com",
  • "response_types_supported": [
    ],
  • "subject_types_supported": [
    ],
  • "id_token_signing_alg_values_supported": [
    ],
  • "spiffe_jwks_uri": "http://example.com"
}

Retrieve SPIFFE JWKS for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/.well-known/openid-configuration

Response samples

Content type
application/json
{
  • "issuer": "string",
  • "jwks_uri": "http://example.com",
  • "response_types_supported": [
    ],
  • "subject_types_supported": [
    ],
  • "id_token_signing_alg_values_supported": [
    ],
  • "spiffe_jwks_uri": "http://example.com"
}

Retrieve SPIFFE JWKS for current Org

SPIFFE trust-domain JWKS — same key material as the OIDC JWKS but with use: jwt-svid for SPIFFE-native verifiers. No authentication required.

Not-configured and malformed-body behavior matches the OIDC JWKS endpoint.

@@ -15184,7 +15382,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable.

Response samples

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

Deprecations

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/.well-known/spiffe/jwks.json

Response samples

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

Deprecations

Typical API Call Flow for Tenant