From 2e1497460ffb108a2f4c847b15e46e3b713dc42c Mon Sep 17 00:00:00 2001 From: Kyle Felter Date: Mon, 13 Jul 2026 12:52:32 -0500 Subject: [PATCH] feat: Add Site Explorer REST actions Signed-off-by: Kyle Felter --- .../api/handler/siteexplorerendpointaction.go | 124 +++++++++ .../siteexplorerendpointaction_test.go | 191 +++++++++++++ .../api/model/siteexplorerendpointaction.go | 104 ++++++++ .../model/siteexplorerendpointaction_test.go | 56 ++++ rest-api/api/pkg/api/routes.go | 7 + rest-api/api/pkg/api/routes_test.go | 3 + rest-api/cli/pkg/commands_test.go | 21 ++ rest-api/openapi/spec.yaml | 109 ++++++++ rest-api/sdk/standard/api_site_explorer.go | 208 +++++++++++++++ rest-api/sdk/standard/client.go | 3 + .../model_site_explorer_endpoint_action.go | 244 +++++++++++++++++ ...l_site_explorer_endpoint_action_request.go | 252 ++++++++++++++++++ 12 files changed, 1322 insertions(+) create mode 100644 rest-api/api/pkg/api/handler/siteexplorerendpointaction.go create mode 100644 rest-api/api/pkg/api/handler/siteexplorerendpointaction_test.go create mode 100644 rest-api/api/pkg/api/model/siteexplorerendpointaction.go create mode 100644 rest-api/api/pkg/api/model/siteexplorerendpointaction_test.go create mode 100644 rest-api/sdk/standard/api_site_explorer.go create mode 100644 rest-api/sdk/standard/model_site_explorer_endpoint_action.go create mode 100644 rest-api/sdk/standard/model_site_explorer_endpoint_action_request.go diff --git a/rest-api/api/pkg/api/handler/siteexplorerendpointaction.go b/rest-api/api/pkg/api/handler/siteexplorerendpointaction.go new file mode 100644 index 0000000000..1887be7716 --- /dev/null +++ b/rest-api/api/pkg/api/handler/siteexplorerendpointaction.go @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package handler + +import ( + "net/http" + "slices" + + "github.com/labstack/echo/v4" + "google.golang.org/protobuf/proto" + + "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" + cutil "github.com/NVIDIA/infra-controller/rest-api/common/pkg/util" + cdb "github.com/NVIDIA/infra-controller/rest-api/db/pkg/db" + corev1 "github.com/NVIDIA/infra-controller/rest-api/proto/core/gen/v1" +) + +// SiteExplorerEndpointActionHandler triggers clear-error or re-explore actions for explored endpoints. +type SiteExplorerEndpointActionHandler struct { + dbSession *cdb.Session + scp *sc.ClientPool + cfg *config.Config + tracerSpan *cutil.TracerSpan +} + +// NewSiteExplorerEndpointActionHandler returns a handler for site-explorer endpoint actions. +func NewSiteExplorerEndpointActionHandler(dbSession *cdb.Session, scp *sc.ClientPool, cfg *config.Config) SiteExplorerEndpointActionHandler { + return SiteExplorerEndpointActionHandler{ + dbSession: dbSession, + scp: scp, + cfg: cfg, + tracerSpan: cutil.NewTracerSpan(), + } +} + +// Handle godoc +// @Summary Trigger Site Explorer Endpoint Action +// @Description Trigger clear-error or re-explore for all or selected explored endpoints. +// @Tags site-explorer +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param org path string true "Name of NGC organization" +// @Param request body model.APISiteExplorerEndpointActionRequest true "Site explorer endpoint action" +// @Success 200 {object} model.APISiteExplorerEndpointAction +// @Router /v2/org/{org}/nico/site-explorer/endpoint/action [post] +func (h SiteExplorerEndpointActionHandler) Handle(c echo.Context) error { + org, dbUser, ctx, logger, handlerSpan := common.SetupHandler("SiteExplorerEndpointAction", "Create", c, h.tracerSpan) + if handlerSpan != nil { + defer handlerSpan.End() + } + + var apiReq model.APISiteExplorerEndpointActionRequest + if err := c.Bind(&apiReq); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, "Invalid request body", nil) + } + if err := apiReq.Validate(); err != nil { + return cutil.NewAPIErrorResponse(c, http.StatusBadRequest, err.Error(), nil) + } + + stc, siteID, apiErr := common.AuthorizeProviderSiteForCore(common.AuthorizeProviderSiteForCoreInput{ + Ctx: ctx, + Logger: logger, + DBSession: h.dbSession, + SCP: h.scp, + Org: org, + User: dbUser, + SiteID: apiReq.SiteID, + }) + if apiErr != nil { + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, apiErr.Data) + } + + endpointIDs := slices.Clone(apiReq.EndpointIDs) + if apiReq.Target == model.SiteExplorerEndpointTargetAll { + var ids corev1.ExploredEndpointIdList + apiErr = common.ExecuteCoreGRPC( + ctx, + stc, + corev1.Forge_FindExploredEndpointIds_FullMethodName, + &corev1.ExploredEndpointSearchFilter{}, + &ids, + siteID, + ) + if apiErr != nil { + logAPIError(logger, apiErr, "failed to find explored endpoint IDs") + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, nil) + } + endpointIDs = ids.GetEndpointIds() + } + + logger.Info(). + Str("action", apiReq.Action). + Str("target", apiReq.Target). + Str("siteID", apiReq.SiteID). + Int("endpointCount", len(endpointIDs)). + Msg("triggering site-explorer endpoint action via Core proxy") + + for _, endpointID := range endpointIDs { + var fullMethod string + var coreReq proto.Message + switch apiReq.Action { + case model.SiteExplorerEndpointActionClearError: + fullMethod = corev1.Forge_ClearSiteExplorationError_FullMethodName + coreReq = &corev1.ClearSiteExplorationErrorRequest{IpAddress: endpointID} + case model.SiteExplorerEndpointActionReExplore: + fullMethod = corev1.Forge_ReExploreEndpoint_FullMethodName + coreReq = &corev1.ReExploreEndpointRequest{IpAddress: endpointID} + } + + apiErr = common.ExecuteCoreGRPC(ctx, stc, fullMethod, coreReq, nil, siteID) + if apiErr != nil { + actionLogger := logger.With().Str("action", apiReq.Action).Str("endpointID", endpointID).Logger() + logAPIError(actionLogger, apiErr, "failed to trigger site-explorer endpoint action") + return cutil.NewAPIErrorResponse(c, apiErr.Code, apiErr.Message, nil) + } + } + + return c.JSON(http.StatusOK, apiReq.ToResponse(endpointIDs)) +} diff --git a/rest-api/api/pkg/api/handler/siteexplorerendpointaction_test.go b/rest-api/api/pkg/api/handler/siteexplorerendpointaction_test.go new file mode 100644 index 0000000000..c152deb583 --- /dev/null +++ b/rest-api/api/pkg/api/handler/siteexplorerendpointaction_test.go @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package handler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + tmocks "go.temporal.io/sdk/mocks" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "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" + 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 TestSiteExplorerEndpointActionHandlerClearErrorForSelectedEndpoints(t *testing.T) { + fixture := newSiteExplorerEndpointActionHandlerFixture(t, []string{authz.ProviderAdminRole}) + fixture.expectProxyResponse(t, nil) + + rec := fixture.request(t, model.APISiteExplorerEndpointActionRequest{ + SiteID: fixture.siteID, + Action: model.SiteExplorerEndpointActionClearError, + Target: model.SiteExplorerEndpointTargetEndpointIDs, + EndpointIDs: []string{"10.0.0.1"}, + }) + assert.Equal(t, http.StatusOK, rec.Code) + require.Len(t, fixture.proxiedReqs, 1) + assert.Equal(t, corev1.Forge_ClearSiteExplorationError_FullMethodName, fixture.proxiedReqs[0].FullMethod) + assert.Empty(t, fixture.proxiedReqs[0].EncryptedSecrets) + + var coreReq corev1.ClearSiteExplorationErrorRequest + require.NoError(t, protojson.Unmarshal(fixture.proxiedReqs[0].RequestJSON, &coreReq)) + assert.Equal(t, "10.0.0.1", coreReq.GetIpAddress()) + + var resp model.APISiteExplorerEndpointAction + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.Equal(t, fixture.siteID, resp.SiteID) + assert.Equal(t, []string{"10.0.0.1"}, resp.EndpointIDs) +} + +func TestSiteExplorerEndpointActionHandlerReExploreForAllEndpoints(t *testing.T) { + fixture := newSiteExplorerEndpointActionHandlerFixture(t, []string{authz.ProviderAdminRole}) + fixture.expectProxyResponse(t, &corev1.ExploredEndpointIdList{EndpointIds: []string{"10.0.0.1", "10.0.0.2"}}) + fixture.expectProxyResponse(t, nil) + fixture.expectProxyResponse(t, nil) + + rec := fixture.request(t, model.APISiteExplorerEndpointActionRequest{ + SiteID: fixture.siteID, + Action: model.SiteExplorerEndpointActionReExplore, + Target: model.SiteExplorerEndpointTargetAll, + }) + assert.Equal(t, http.StatusOK, rec.Code) + require.Len(t, fixture.proxiedReqs, 3) + assert.Equal(t, corev1.Forge_FindExploredEndpointIds_FullMethodName, fixture.proxiedReqs[0].FullMethod) + assert.Equal(t, corev1.Forge_ReExploreEndpoint_FullMethodName, fixture.proxiedReqs[1].FullMethod) + assert.Equal(t, corev1.Forge_ReExploreEndpoint_FullMethodName, fixture.proxiedReqs[2].FullMethod) + + var firstAction corev1.ReExploreEndpointRequest + require.NoError(t, protojson.Unmarshal(fixture.proxiedReqs[1].RequestJSON, &firstAction)) + assert.Equal(t, "10.0.0.1", firstAction.GetIpAddress()) + var secondAction corev1.ReExploreEndpointRequest + require.NoError(t, protojson.Unmarshal(fixture.proxiedReqs[2].RequestJSON, &secondAction)) + assert.Equal(t, "10.0.0.2", secondAction.GetIpAddress()) +} + +func TestSiteExplorerEndpointActionHandlerRejectsInvalidRequest(t *testing.T) { + fixture := newSiteExplorerEndpointActionHandlerFixture(t, []string{authz.ProviderAdminRole}) + + rec := fixture.request(t, model.APISiteExplorerEndpointActionRequest{ + SiteID: fixture.siteID, + Action: model.SiteExplorerEndpointActionClearError, + Target: model.SiteExplorerEndpointTargetEndpointIDs, + EndpointIDs: []string{"not-an-ip"}, + }) + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Empty(t, fixture.proxiedReqs) +} + +func TestSiteExplorerEndpointActionHandlerRejectsNonProviderAdmin(t *testing.T) { + fixture := newSiteExplorerEndpointActionHandlerFixture(t, nil) + + rec := fixture.request(t, model.APISiteExplorerEndpointActionRequest{ + SiteID: fixture.siteID, + Action: model.SiteExplorerEndpointActionClearError, + Target: model.SiteExplorerEndpointTargetEndpointIDs, + EndpointIDs: []string{"10.0.0.1"}, + }) + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Empty(t, fixture.proxiedReqs) +} + +type siteExplorerEndpointActionHandlerFixture struct { + org string + siteID string + user interface{} + handler SiteExplorerEndpointActionHandler + tsc *tmocks.Client + proxiedReqs []coreproxy.Request +} + +func newSiteExplorerEndpointActionHandlerFixture(t *testing.T, roles []string) *siteExplorerEndpointActionHandlerFixture { + t.Helper() + + dbSession := common.TestInitDB(t) + t.Cleanup(dbSession.Close) + common.TestSetupSchema(t, dbSession) + + org := "test-org-" + uuid.NewString() + user := common.TestBuildUser(t, dbSession, "test-starfleet-id-"+uuid.NewString(), org, roles) + ip := common.TestBuildInfrastructureProvider(t, dbSession, "Test Infrastructure Provider", org, user) + site := common.TestBuildSite(t, dbSession, ip, "Test Site", user) + sDAO := cdbm.NewSiteDAO(dbSession) + _, err := sDAO.Update(context.Background(), nil, cdbm.SiteUpdateInput{ + SiteID: site.ID, + Status: cutil.GetPtr(cdbm.SiteStatusRegistered), + }) + require.NoError(t, err) + + tsc := &tmocks.Client{} + scp := sc.NewClientPool(nil) + scp.IDClientMap[site.ID.String()] = tsc + + return &siteExplorerEndpointActionHandlerFixture{ + org: org, + siteID: site.ID.String(), + user: user, + handler: NewSiteExplorerEndpointActionHandler(dbSession, scp, common.GetTestConfig()), + tsc: tsc, + } +} + +func (f *siteExplorerEndpointActionHandlerFixture) expectProxyResponse(t *testing.T, resp proto.Message) { + t.Helper() + + wrun := &tmocks.WorkflowRun{} + wrun.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + if resp == nil { + return + } + out := args.Get(1).(*coreproxy.Response) + responseJSON, err := protojson.Marshal(resp) + require.NoError(t, err) + out.ResponseJSON = responseJSON + }).Return(nil).Once() + + f.tsc.On( + "ExecuteWorkflow", + mock.Anything, + mock.Anything, + coreproxy.WorkflowName, + mock.Anything, + ).Run(func(args mock.Arguments) { + f.proxiedReqs = append(f.proxiedReqs, args.Get(3).(coreproxy.Request)) + }).Return(wrun, nil).Once() +} + +func (f *siteExplorerEndpointActionHandlerFixture) request(t *testing.T, apiReq model.APISiteExplorerEndpointActionRequest) *httptest.ResponseRecorder { + t.Helper() + + body, err := json.Marshal(apiReq) + require.NoError(t, err) + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + ec := e.NewContext(req, rec) + ec.SetParamNames("orgName") + ec.SetParamValues(f.org) + ec.Set("user", f.user) + + require.NoError(t, f.handler.Handle(ec)) + return rec +} diff --git a/rest-api/api/pkg/api/model/siteexplorerendpointaction.go b/rest-api/api/pkg/api/model/siteexplorerendpointaction.go new file mode 100644 index 0000000000..3c52cb8fb1 --- /dev/null +++ b/rest-api/api/pkg/api/model/siteexplorerendpointaction.go @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package model + +import ( + "fmt" + "net/netip" + "slices" + + validation "github.com/go-ozzo/ozzo-validation/v4" + validationis "github.com/go-ozzo/ozzo-validation/v4/is" +) + +const ( + // SiteExplorerEndpointActionClearError clears the endpoint's last exploration error. + SiteExplorerEndpointActionClearError = "ClearError" + // SiteExplorerEndpointActionReExplore schedules endpoint re-exploration. + SiteExplorerEndpointActionReExplore = "ReExplore" + + // SiteExplorerEndpointTargetAll targets every explored endpoint at the site. + SiteExplorerEndpointTargetAll = "All" + // SiteExplorerEndpointTargetEndpointIDs targets the explicit endpointIds list. + SiteExplorerEndpointTargetEndpointIDs = "EndpointIds" +) + +// APISiteExplorerEndpointActionRequest triggers a site-explorer action for explored endpoints. +type APISiteExplorerEndpointActionRequest struct { + // SiteID is the ID of the Site whose explored endpoints are targeted. + SiteID string `json:"siteId"` + // Action selects the site-explorer operation to run: "ClearError" or "ReExplore". + Action string `json:"action"` + // Target selects the endpoint set: "All" or "EndpointIds". + Target string `json:"target"` + // EndpointIDs is required when target is "EndpointIds"; endpoint IDs are BMC IP addresses. + EndpointIDs []string `json:"endpointIds,omitempty"` +} + +// APISiteExplorerEndpointAction is the accepted site-explorer action response. +type APISiteExplorerEndpointAction struct { + // SiteID is the ID of the Site whose explored endpoints were targeted. + SiteID string `json:"siteId"` + // Action is the site-explorer operation that completed. + Action string `json:"action"` + // Target is the endpoint set that was selected. + Target string `json:"target"` + // EndpointIDs is the set of endpoint IDs that completed in Core. + EndpointIDs []string `json:"endpointIds"` +} + +// Validate checks the request shape before it is converted to Core protos. +func (r *APISiteExplorerEndpointActionRequest) Validate() error { + if err := validation.ValidateStruct(r, + validation.Field(&r.SiteID, + validation.Required.Error(validationErrorValueRequired), + validationis.UUID.Error(validationErrorInvalidUUID)), + validation.Field(&r.Action, + validation.Required.Error(validationErrorValueRequired)), + validation.Field(&r.Target, + validation.Required.Error(validationErrorValueRequired)), + ); err != nil { + return err + } + + switch r.Action { + case SiteExplorerEndpointActionClearError, SiteExplorerEndpointActionReExplore: + default: + return fmt.Errorf("invalid action %q (expected %q or %q)", r.Action, SiteExplorerEndpointActionClearError, SiteExplorerEndpointActionReExplore) + } + + switch r.Target { + case SiteExplorerEndpointTargetAll: + if len(r.EndpointIDs) > 0 { + return fmt.Errorf("endpointIds must be empty when target is %q", SiteExplorerEndpointTargetAll) + } + case SiteExplorerEndpointTargetEndpointIDs: + if len(r.EndpointIDs) == 0 { + return fmt.Errorf("endpointIds is required when target is %q", SiteExplorerEndpointTargetEndpointIDs) + } + for _, endpointID := range r.EndpointIDs { + if _, err := netip.ParseAddr(endpointID); err != nil { + return fmt.Errorf("invalid endpointId %q: %s", endpointID, validationErrorInvalidIPAddress) + } + } + default: + return fmt.Errorf("invalid target %q (expected %q or %q)", r.Target, SiteExplorerEndpointTargetAll, SiteExplorerEndpointTargetEndpointIDs) + } + + return nil +} + +// ToResponse returns the completed action without Core transport details. +func (r *APISiteExplorerEndpointActionRequest) ToResponse(endpointIDs []string) *APISiteExplorerEndpointAction { + respEndpointIDs := slices.Clone(endpointIDs) + if respEndpointIDs == nil { + respEndpointIDs = []string{} + } + return &APISiteExplorerEndpointAction{ + SiteID: r.SiteID, + Action: r.Action, + Target: r.Target, + EndpointIDs: respEndpointIDs, + } +} diff --git a/rest-api/api/pkg/api/model/siteexplorerendpointaction_test.go b/rest-api/api/pkg/api/model/siteexplorerendpointaction_test.go new file mode 100644 index 0000000000..dbbc49fd9e --- /dev/null +++ b/rest-api/api/pkg/api/model/siteexplorerendpointaction_test.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package model + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestAPISiteExplorerEndpointActionRequestValidate(t *testing.T) { + siteID := uuid.NewString() + tests := []struct { + name string + req APISiteExplorerEndpointActionRequest + wantErr bool + }{ + {name: "all clear-error", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionClearError, Target: SiteExplorerEndpointTargetAll}}, + {name: "selected re-explore", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionReExplore, Target: SiteExplorerEndpointTargetEndpointIDs, EndpointIDs: []string{"10.0.0.1"}}}, + {name: "missing site ID", req: APISiteExplorerEndpointActionRequest{Action: SiteExplorerEndpointActionClearError, Target: SiteExplorerEndpointTargetAll}, wantErr: true}, + {name: "invalid site ID", req: APISiteExplorerEndpointActionRequest{SiteID: "bad-site-id", Action: SiteExplorerEndpointActionClearError, Target: SiteExplorerEndpointTargetAll}, wantErr: true}, + {name: "missing action", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Target: SiteExplorerEndpointTargetAll}, wantErr: true}, + {name: "invalid action", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: "clear-error", Target: SiteExplorerEndpointTargetAll}, wantErr: true}, + {name: "missing target", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionClearError}, wantErr: true}, + {name: "invalid target", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionClearError, Target: "all"}, wantErr: true}, + {name: "all with endpoint IDs", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionClearError, Target: SiteExplorerEndpointTargetAll, EndpointIDs: []string{"10.0.0.1"}}, wantErr: true}, + {name: "selected without endpoint IDs", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionClearError, Target: SiteExplorerEndpointTargetEndpointIDs}, wantErr: true}, + {name: "selected invalid endpoint ID", req: APISiteExplorerEndpointActionRequest{SiteID: siteID, Action: SiteExplorerEndpointActionClearError, Target: SiteExplorerEndpointTargetEndpointIDs, EndpointIDs: []string{"not-an-ip"}}, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func TestAPISiteExplorerEndpointActionRequestToResponse(t *testing.T) { + req := APISiteExplorerEndpointActionRequest{ + SiteID: uuid.NewString(), + Action: SiteExplorerEndpointActionClearError, + Target: SiteExplorerEndpointTargetEndpointIDs, + } + + endpointIDs := []string{"10.0.0.1"} + resp := req.ToResponse(endpointIDs) + endpointIDs[0] = "10.0.0.2" + assert.Equal(t, []string{"10.0.0.1"}, resp.EndpointIDs) +} diff --git a/rest-api/api/pkg/api/routes.go b/rest-api/api/pkg/api/routes.go index 6cfe3fdcf2..d3a4568e24 100644 --- a/rest-api/api/pkg/api/routes.go +++ b/rest-api/api/pkg/api/routes.go @@ -37,6 +37,13 @@ func NewAPIRoutes(dbSession *cdb.Session, tc tClient.Client, tnc tClient.Namespa Method: http.MethodPut, Handler: apiHandler.NewCreateOrUpdateBMCCredentialHandler(dbSession, scp, cfg), }, + // Site Explorer endpoint actions (Provider Admin). Composes existing + // single-endpoint Core methods through the generic gRPC proxy. + { + Path: apiPathPrefix + "/site-explorer/endpoint/action", + Method: http.MethodPost, + Handler: apiHandler.NewSiteExplorerEndpointActionHandler(dbSession, scp, cfg), + }, // User endpoint { Path: apiPathPrefix + "/user/current", diff --git a/rest-api/api/pkg/api/routes_test.go b/rest-api/api/pkg/api/routes_test.go index 7cc900e9a5..c683fcf226 100644 --- a/rest-api/api/pkg/api/routes_test.go +++ b/rest-api/api/pkg/api/routes_test.go @@ -36,6 +36,7 @@ func TestNewAPIRoutes(t *testing.T) { routeCount := map[string]int{ "metadata": 1, "credential": 1, + "site-explorer": 1, "service-account": 1, "infrastructure-provider": 4, "tenant": 4, @@ -112,6 +113,8 @@ func TestNewAPIRoutes(t *testing.T) { bmcCredentialPath := "/org/:orgName/" + cfg.GetAPIName() + "/credential/bmc" assertRouteExists(t, got, http.MethodPut, bmcCredentialPath) + siteExplorerActionPath := "/org/:orgName/" + cfg.GetAPIName() + "/site-explorer/endpoint/action" + assertRouteExists(t, got, http.MethodPost, siteExplorerActionPath) machineAdminPath := "/org/:orgName/" + cfg.GetAPIName() + "/machine/:id" assertRouteExists(t, got, http.MethodPatch, machineAdminPath+"/bmc/reset") diff --git a/rest-api/cli/pkg/commands_test.go b/rest-api/cli/pkg/commands_test.go index ce206e2ede..d88813fd43 100644 --- a/rest-api/cli/pkg/commands_test.go +++ b/rest-api/cli/pkg/commands_test.go @@ -884,6 +884,27 @@ func TestBuildCommands_CurrentSingletonsAreRunnable(t *testing.T) { } } +func TestBuildCommands_SiteExplorerActionIsRunnable(t *testing.T) { + spec, err := ParseSpec(openapi.Spec) + require.NoError(t, err) + + var siteExplorer *cli.Command + for _, command := range BuildCommands(spec) { + if command.HasName("site-explorer") { + siteExplorer = command + break + } + } + require.NotNil(t, siteExplorer) + + for _, command := range siteExplorer.Subcommands { + if command.HasName("create") { + return + } + } + t.Fatal("site-explorer create must be generated from the OpenAPI operation") +} + // TestBuildCommands_AllocationConstraintIsUpdateOnly is the CLI-side guard for // NVBug 6232163: the server only registers PATCH for the AllocationConstraint // sub-resource, and the stale create/get/list/delete endpoints were removed diff --git a/rest-api/openapi/spec.yaml b/rest-api/openapi/spec.yaml index 9fbe85f03d..c0b9af46fe 100644 --- a/rest-api/openapi/spec.yaml +++ b/rest-api/openapi/spec.yaml @@ -121,6 +121,9 @@ tags: - name: BMC Credential description: |- BMC Credential endpoints allow creating and updating BMC credentials across all Machines of a Site + - name: Site Explorer + description: |- + Site Explorer discovers BMC endpoints on a Site and tracks their exploration state. - name: Allocation description: |- Allocations are the mechanism by which Provider can delegate Network and Compute resources to Tenant. @@ -1204,6 +1207,51 @@ paths: Create or update a site-wide or per-BMC root credential. Equivalent to `carbide-admin-cli credential add-bmc`. + User must have authorization role with `PROVIDER_ADMIN` suffix. + '/v2/org/{org}/nico/site-explorer/endpoint/action': + parameters: + - schema: + type: string + name: org + in: path + required: true + description: Name of the Org + post: + summary: Trigger Site Explorer Endpoint Action + tags: + - Site Explorer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SiteExplorerEndpointActionRequest' + responses: + '200': + description: The action completed for every selected endpoint. + content: + application/json: + schema: + $ref: '#/components/schemas/SiteExplorerEndpointAction' + '400': + $ref: '#/components/responses/GenericHttpError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/GenericHttpError' + '412': + $ref: '#/components/responses/GenericHttpError' + '500': + $ref: '#/components/responses/GenericHttpError' + '504': + $ref: '#/components/responses/GenericHttpError' + operationId: create-site-explorer-endpoint-action + description: |- + Clear the last exploration error or queue re-exploration for all + explored endpoints at a Site or an explicit BMC IP address list. + + The operation calls Core once per endpoint. If a call fails, processing + stops and endpoints earlier in the list may already have completed. User must have authorization role with `PROVIDER_ADMIN` suffix. '/v2/org/{org}/nico/allocation': parameters: @@ -13559,6 +13607,67 @@ components: macAddress: type: string description: BMC MAC address. Required for kind BMCRoot, ignored for SiteWideRoot. + SiteExplorerEndpointActionRequest: + type: object + title: SiteExplorerEndpointActionRequest + description: Request to trigger a Site Explorer action for explored endpoints. + required: + - siteId + - action + - target + properties: + siteId: + type: string + format: uuid + description: ID of the Site whose explored endpoints are targeted. + action: + type: string + description: Site Explorer endpoint action to trigger. + enum: + - ClearError + - ReExplore + target: + type: string + description: Endpoint set to target. + enum: + - All + - EndpointIds + endpointIds: + type: array + description: BMC IP addresses to target when target is EndpointIds. + items: + type: string + SiteExplorerEndpointAction: + type: object + title: SiteExplorerEndpointAction + description: Completed Site Explorer endpoint action. + required: + - siteId + - action + - target + - endpointIds + properties: + siteId: + type: string + format: uuid + description: ID of the Site whose explored endpoints were targeted. + action: + type: string + description: Site Explorer endpoint action that completed. + enum: + - ClearError + - ReExplore + target: + type: string + description: Endpoint set that was selected. + enum: + - All + - EndpointIds + endpointIds: + type: array + description: BMC IP addresses for which the action completed. + items: + type: string BMCResetRequest: type: object title: BMCResetRequest diff --git a/rest-api/sdk/standard/api_site_explorer.go b/rest-api/sdk/standard/api_site_explorer.go new file mode 100644 index 0000000000..e2eb1d46fd --- /dev/null +++ b/rest-api/sdk/standard/api_site_explorer.go @@ -0,0 +1,208 @@ +/* +NVIDIA Infra Controller REST API + +NVIDIA Infra Controller REST API allows users to create and manage resources, e.g., VPCs, Subnets, and Instances, across all connected NVIDIA Infra Controller datacenters, also referred to as Sites. + +API version: 2.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package standard + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + +// SiteExplorerAPIService SiteExplorerAPI service +type SiteExplorerAPIService service + +type ApiCreateSiteExplorerEndpointActionRequest struct { + ctx context.Context + ApiService *SiteExplorerAPIService + org string + siteExplorerEndpointActionRequest *SiteExplorerEndpointActionRequest +} + +func (r ApiCreateSiteExplorerEndpointActionRequest) SiteExplorerEndpointActionRequest(siteExplorerEndpointActionRequest SiteExplorerEndpointActionRequest) ApiCreateSiteExplorerEndpointActionRequest { + r.siteExplorerEndpointActionRequest = &siteExplorerEndpointActionRequest + return r +} + +func (r ApiCreateSiteExplorerEndpointActionRequest) Execute() (*SiteExplorerEndpointAction, *http.Response, error) { + return r.ApiService.CreateSiteExplorerEndpointActionExecute(r) +} + +/* +CreateSiteExplorerEndpointAction Trigger Site Explorer Endpoint Action + +Clear the last exploration error or queue re-exploration for all +explored endpoints at a Site or an explicit BMC IP address list. + +The operation calls Core once per endpoint. If a call fails, processing +stops and endpoints earlier in the list may already have completed. +User must have authorization role with `PROVIDER_ADMIN` suffix. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param org Name of the Org + @return ApiCreateSiteExplorerEndpointActionRequest +*/ +func (a *SiteExplorerAPIService) CreateSiteExplorerEndpointAction(ctx context.Context, org string) ApiCreateSiteExplorerEndpointActionRequest { + return ApiCreateSiteExplorerEndpointActionRequest{ + ApiService: a, + ctx: ctx, + org: org, + } +} + +// Execute executes the request +// +// @return SiteExplorerEndpointAction +func (a *SiteExplorerAPIService) CreateSiteExplorerEndpointActionExecute(r ApiCreateSiteExplorerEndpointActionRequest) (*SiteExplorerEndpointAction, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *SiteExplorerEndpointAction + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "SiteExplorerAPIService.CreateSiteExplorerEndpointAction") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v2/org/{org}/nico/site-explorer/endpoint/action" + localVarPath = strings.Replace(localVarPath, "{"+"org"+"}", url.PathEscape(parameterValueToString(r.org, "org")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.siteExplorerEndpointActionRequest == nil { + return localVarReturnValue, nil, reportError("siteExplorerEndpointActionRequest is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.siteExplorerEndpointActionRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v NICoAPIError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v NICoAPIError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v NICoAPIError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 412 { + var v NICoAPIError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v NICoAPIError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 504 { + var v NICoAPIError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/rest-api/sdk/standard/client.go b/rest-api/sdk/standard/client.go index 2dae2e1ca1..8cf239f4ef 100644 --- a/rest-api/sdk/standard/client.go +++ b/rest-api/sdk/standard/client.go @@ -109,6 +109,8 @@ type APIClient struct { SiteAPI *SiteAPIService + SiteExplorerAPI *SiteExplorerAPIService + SubnetAPI *SubnetAPIService TaskAPI *TaskAPIService @@ -175,6 +177,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.SSHKeyGroupAPI = (*SSHKeyGroupAPIService)(&c.common) c.ServiceAccountAPI = (*ServiceAccountAPIService)(&c.common) c.SiteAPI = (*SiteAPIService)(&c.common) + c.SiteExplorerAPI = (*SiteExplorerAPIService)(&c.common) c.SubnetAPI = (*SubnetAPIService)(&c.common) c.TaskAPI = (*TaskAPIService)(&c.common) c.TenantAPI = (*TenantAPIService)(&c.common) diff --git a/rest-api/sdk/standard/model_site_explorer_endpoint_action.go b/rest-api/sdk/standard/model_site_explorer_endpoint_action.go new file mode 100644 index 0000000000..b4cde553a6 --- /dev/null +++ b/rest-api/sdk/standard/model_site_explorer_endpoint_action.go @@ -0,0 +1,244 @@ +/* +NVIDIA Infra Controller REST API + +NVIDIA Infra Controller REST API allows users to create and manage resources, e.g., VPCs, Subnets, and Instances, across all connected NVIDIA Infra Controller datacenters, also referred to as Sites. + +API version: 2.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package standard + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the SiteExplorerEndpointAction type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SiteExplorerEndpointAction{} + +// SiteExplorerEndpointAction Completed Site Explorer endpoint action. +type SiteExplorerEndpointAction struct { + // ID of the Site whose explored endpoints were targeted. + SiteId string `json:"siteId"` + // Site Explorer endpoint action that completed. + Action string `json:"action"` + // Endpoint set that was selected. + Target string `json:"target"` + // BMC IP addresses for which the action completed. + EndpointIds []string `json:"endpointIds"` +} + +type _SiteExplorerEndpointAction SiteExplorerEndpointAction + +// NewSiteExplorerEndpointAction instantiates a new SiteExplorerEndpointAction object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSiteExplorerEndpointAction(siteId string, action string, target string, endpointIds []string) *SiteExplorerEndpointAction { + this := SiteExplorerEndpointAction{} + this.SiteId = siteId + this.Action = action + this.Target = target + this.EndpointIds = endpointIds + return &this +} + +// NewSiteExplorerEndpointActionWithDefaults instantiates a new SiteExplorerEndpointAction object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSiteExplorerEndpointActionWithDefaults() *SiteExplorerEndpointAction { + this := SiteExplorerEndpointAction{} + return &this +} + +// GetSiteId returns the SiteId field value +func (o *SiteExplorerEndpointAction) GetSiteId() string { + if o == nil { + var ret string + return ret + } + + return o.SiteId +} + +// GetSiteIdOk returns a tuple with the SiteId field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointAction) GetSiteIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SiteId, true +} + +// SetSiteId sets field value +func (o *SiteExplorerEndpointAction) SetSiteId(v string) { + o.SiteId = v +} + +// GetAction returns the Action field value +func (o *SiteExplorerEndpointAction) GetAction() string { + if o == nil { + var ret string + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointAction) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *SiteExplorerEndpointAction) SetAction(v string) { + o.Action = v +} + +// GetTarget returns the Target field value +func (o *SiteExplorerEndpointAction) GetTarget() string { + if o == nil { + var ret string + return ret + } + + return o.Target +} + +// GetTargetOk returns a tuple with the Target field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointAction) GetTargetOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Target, true +} + +// SetTarget sets field value +func (o *SiteExplorerEndpointAction) SetTarget(v string) { + o.Target = v +} + +// GetEndpointIds returns the EndpointIds field value +func (o *SiteExplorerEndpointAction) GetEndpointIds() []string { + if o == nil { + var ret []string + return ret + } + + return o.EndpointIds +} + +// GetEndpointIdsOk returns a tuple with the EndpointIds field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointAction) GetEndpointIdsOk() ([]string, bool) { + if o == nil { + return nil, false + } + return o.EndpointIds, true +} + +// SetEndpointIds sets field value +func (o *SiteExplorerEndpointAction) SetEndpointIds(v []string) { + o.EndpointIds = v +} + +func (o SiteExplorerEndpointAction) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SiteExplorerEndpointAction) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["siteId"] = o.SiteId + toSerialize["action"] = o.Action + toSerialize["target"] = o.Target + toSerialize["endpointIds"] = o.EndpointIds + return toSerialize, nil +} + +func (o *SiteExplorerEndpointAction) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "siteId", + "action", + "target", + "endpointIds", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if value, exists := allProperties[requiredProperty]; !exists || value == nil { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSiteExplorerEndpointAction := _SiteExplorerEndpointAction{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSiteExplorerEndpointAction) + + if err != nil { + return err + } + + *o = SiteExplorerEndpointAction(varSiteExplorerEndpointAction) + + return err +} + +type NullableSiteExplorerEndpointAction struct { + value *SiteExplorerEndpointAction + isSet bool +} + +func (v NullableSiteExplorerEndpointAction) Get() *SiteExplorerEndpointAction { + return v.value +} + +func (v *NullableSiteExplorerEndpointAction) Set(val *SiteExplorerEndpointAction) { + v.value = val + v.isSet = true +} + +func (v NullableSiteExplorerEndpointAction) IsSet() bool { + return v.isSet +} + +func (v *NullableSiteExplorerEndpointAction) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSiteExplorerEndpointAction(val *SiteExplorerEndpointAction) *NullableSiteExplorerEndpointAction { + return &NullableSiteExplorerEndpointAction{value: val, isSet: true} +} + +func (v NullableSiteExplorerEndpointAction) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSiteExplorerEndpointAction) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/rest-api/sdk/standard/model_site_explorer_endpoint_action_request.go b/rest-api/sdk/standard/model_site_explorer_endpoint_action_request.go new file mode 100644 index 0000000000..be4346ec68 --- /dev/null +++ b/rest-api/sdk/standard/model_site_explorer_endpoint_action_request.go @@ -0,0 +1,252 @@ +/* +NVIDIA Infra Controller REST API + +NVIDIA Infra Controller REST API allows users to create and manage resources, e.g., VPCs, Subnets, and Instances, across all connected NVIDIA Infra Controller datacenters, also referred to as Sites. + +API version: 2.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package standard + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the SiteExplorerEndpointActionRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SiteExplorerEndpointActionRequest{} + +// SiteExplorerEndpointActionRequest Request to trigger a Site Explorer action for explored endpoints. +type SiteExplorerEndpointActionRequest struct { + // ID of the Site whose explored endpoints are targeted. + SiteId string `json:"siteId"` + // Site Explorer endpoint action to trigger. + Action string `json:"action"` + // Endpoint set to target. + Target string `json:"target"` + // BMC IP addresses to target when target is EndpointIds. + EndpointIds []string `json:"endpointIds,omitempty"` +} + +type _SiteExplorerEndpointActionRequest SiteExplorerEndpointActionRequest + +// NewSiteExplorerEndpointActionRequest instantiates a new SiteExplorerEndpointActionRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSiteExplorerEndpointActionRequest(siteId string, action string, target string) *SiteExplorerEndpointActionRequest { + this := SiteExplorerEndpointActionRequest{} + this.SiteId = siteId + this.Action = action + this.Target = target + return &this +} + +// NewSiteExplorerEndpointActionRequestWithDefaults instantiates a new SiteExplorerEndpointActionRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSiteExplorerEndpointActionRequestWithDefaults() *SiteExplorerEndpointActionRequest { + this := SiteExplorerEndpointActionRequest{} + return &this +} + +// GetSiteId returns the SiteId field value +func (o *SiteExplorerEndpointActionRequest) GetSiteId() string { + if o == nil { + var ret string + return ret + } + + return o.SiteId +} + +// GetSiteIdOk returns a tuple with the SiteId field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointActionRequest) GetSiteIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SiteId, true +} + +// SetSiteId sets field value +func (o *SiteExplorerEndpointActionRequest) SetSiteId(v string) { + o.SiteId = v +} + +// GetAction returns the Action field value +func (o *SiteExplorerEndpointActionRequest) GetAction() string { + if o == nil { + var ret string + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointActionRequest) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *SiteExplorerEndpointActionRequest) SetAction(v string) { + o.Action = v +} + +// GetTarget returns the Target field value +func (o *SiteExplorerEndpointActionRequest) GetTarget() string { + if o == nil { + var ret string + return ret + } + + return o.Target +} + +// GetTargetOk returns a tuple with the Target field value +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointActionRequest) GetTargetOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Target, true +} + +// SetTarget sets field value +func (o *SiteExplorerEndpointActionRequest) SetTarget(v string) { + o.Target = v +} + +// GetEndpointIds returns the EndpointIds field value if set, zero value otherwise. +func (o *SiteExplorerEndpointActionRequest) GetEndpointIds() []string { + if o == nil || IsNil(o.EndpointIds) { + var ret []string + return ret + } + return o.EndpointIds +} + +// GetEndpointIdsOk returns a tuple with the EndpointIds field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SiteExplorerEndpointActionRequest) GetEndpointIdsOk() ([]string, bool) { + if o == nil || IsNil(o.EndpointIds) { + return nil, false + } + return o.EndpointIds, true +} + +// HasEndpointIds returns a boolean if a field has been set. +func (o *SiteExplorerEndpointActionRequest) HasEndpointIds() bool { + if o != nil && !IsNil(o.EndpointIds) { + return true + } + + return false +} + +// SetEndpointIds gets a reference to the given []string and assigns it to the EndpointIds field. +func (o *SiteExplorerEndpointActionRequest) SetEndpointIds(v []string) { + o.EndpointIds = v +} + +func (o SiteExplorerEndpointActionRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SiteExplorerEndpointActionRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["siteId"] = o.SiteId + toSerialize["action"] = o.Action + toSerialize["target"] = o.Target + if !IsNil(o.EndpointIds) { + toSerialize["endpointIds"] = o.EndpointIds + } + return toSerialize, nil +} + +func (o *SiteExplorerEndpointActionRequest) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "siteId", + "action", + "target", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if value, exists := allProperties[requiredProperty]; !exists || value == nil { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSiteExplorerEndpointActionRequest := _SiteExplorerEndpointActionRequest{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSiteExplorerEndpointActionRequest) + + if err != nil { + return err + } + + *o = SiteExplorerEndpointActionRequest(varSiteExplorerEndpointActionRequest) + + return err +} + +type NullableSiteExplorerEndpointActionRequest struct { + value *SiteExplorerEndpointActionRequest + isSet bool +} + +func (v NullableSiteExplorerEndpointActionRequest) Get() *SiteExplorerEndpointActionRequest { + return v.value +} + +func (v *NullableSiteExplorerEndpointActionRequest) Set(val *SiteExplorerEndpointActionRequest) { + v.value = val + v.isSet = true +} + +func (v NullableSiteExplorerEndpointActionRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableSiteExplorerEndpointActionRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSiteExplorerEndpointActionRequest(val *SiteExplorerEndpointActionRequest) *NullableSiteExplorerEndpointActionRequest { + return &NullableSiteExplorerEndpointActionRequest{value: val, isSet: true} +} + +func (v NullableSiteExplorerEndpointActionRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSiteExplorerEndpointActionRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +}