Skip to content

Commit

Permalink
add API endpoint for BiosControl
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeschuurmans committed Oct 2, 2024
1 parent ba790ee commit 34c611c
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 7 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/metal-toolbox/fleetdb v1.19.5-0.20240913163810-6a9703ca4111
github.com/metal-toolbox/rivets v1.3.7
github.com/metal-toolbox/fleetdb v1.19.5
github.com/metal-toolbox/rivets v1.3.9
github.com/nats-io/nats-server/v2 v2.10.12
github.com/nats-io/nats.go v1.36.0
github.com/pkg/errors v0.9.1
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -540,10 +540,10 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/metal-toolbox/fleetdb v1.19.5-0.20240913163810-6a9703ca4111 h1:WX236DysSYXrlHueyk8WMxUj/vReFUPumXjulZAhHgo=
github.com/metal-toolbox/fleetdb v1.19.5-0.20240913163810-6a9703ca4111/go.mod h1:jaKeC1iiYjXhEPFoUTWtOM5Ni7+5+XZWIXnHIiBdq94=
github.com/metal-toolbox/rivets v1.3.7 h1:ZM6AbX1xASS91FWi/2i2wh9twVOPJTzpD3c7fcllhBk=
github.com/metal-toolbox/rivets v1.3.7/go.mod h1:8irU6eXgOa3QkjdcGi/aY4vqoMqCkbwVz7iVTYYPCX8=
github.com/metal-toolbox/fleetdb v1.19.5 h1:ERgdFAUtWnT/AeVhCGclsENmwPhU88JUcgOZAdxWKYI=
github.com/metal-toolbox/fleetdb v1.19.5/go.mod h1:k9MZXQsJX4NfBoANst6g1468papSs0tzsSyzN3gGWuQ=
github.com/metal-toolbox/rivets v1.3.9 h1:xiBxEVvZNsw3IsE0NVnaMWR5iXNuc7m2QhohmzwaVvg=
github.com/metal-toolbox/rivets v1.3.9/go.mod h1:yxvMwsGL8LsEWL5eBq17ViEvULVOojl+vIcGcz+YTzE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
Expand Down
7 changes: 7 additions & 0 deletions pkg/api/v1/conditions/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ func (c *Client) ServerFirmwareInstall(ctx context.Context,
return c.post(ctx, path, params)
}

func (c *Client) ServerBiosControl(ctx context.Context,
params *rctypes.BiosControlTaskParameters) (*v1types.ServerResponse, error) {
path := fmt.Sprintf("servers/%s/biosControl", params.AssetID.String())

return c.post(ctx, path, params)
}

func (c *Client) ServerConditionCreate(ctx context.Context, serverID uuid.UUID, conditionKind rctypes.Kind, conditionCreate v1types.ConditionCreate) (*v1types.ServerResponse, error) {
path := fmt.Sprintf("servers/%s/condition/%s", serverID.String(), conditionKind)

Expand Down
116 changes: 115 additions & 1 deletion pkg/api/v1/conditions/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,6 @@ func TestFirmwareInstall(t *testing.T) {
mockStore: func(r *store.MockRepository) {
r.On("Create", mock.Anything, serverID, "facility", mock.Anything, mock.Anything).
Return(nil).Once()

},
mockFleetDB: func(r *fleetdb.MockFleetDB) {
fwset.ComponentFirmware = append(fwset.ComponentFirmware, oobfw)
Expand Down Expand Up @@ -845,3 +844,118 @@ func TestServerDeleteInvalidUUID(t *testing.T) {
})
}
}

func TestServerBiosControl(t *testing.T) {
serverID := uuid.New()

validParams := rctypes.BiosControlTaskParameters{
AssetID: serverID,
Action: rctypes.ResetConfig,
}

testcases := []struct {
name string
payload *rctypes.BiosControlTaskParameters
mockStore func(r *store.MockRepository)
mockFleetDB func(r *fleetdb.MockFleetDB)
expectResponse func() *v1types.ServerResponse
expectErrorContains string
expectPublish bool
}{
{
name: "success case",
payload: &validParams,
mockStore: func(r *store.MockRepository) {
r.On("Create", mock.Anything, serverID, "facility", mock.Anything, mock.Anything).
Return(nil).Once()
},
mockFleetDB: func(r *fleetdb.MockFleetDB) {
r.On("GetServer", mock.Anything, mock.Anything).
Return(&model.Server{FacilityCode: "facility"}, nil).Once()
},
expectResponse: func() *v1types.ServerResponse {
return &v1types.ServerResponse{
StatusCode: 200,
Message: "condition set",
}
},
expectErrorContains: "",
expectPublish: true,
},
{
name: "no server",
payload: &validParams,
mockFleetDB: func(r *fleetdb.MockFleetDB) {
r.On("GetServer", mock.Anything, mock.Anything).
Return(nil, fmt.Errorf("no server")).Once()
},
expectResponse: func() *v1types.ServerResponse {
return &v1types.ServerResponse{
StatusCode: 500,
Message: "server facility: no server",
}
},
expectErrorContains: "",
expectPublish: false,
},
{
name: "active condition error",
payload: &validParams,
mockStore: func(r *store.MockRepository) {
r.On("Create", mock.Anything, serverID, "facility", mock.Anything, mock.Anything).
Return(fmt.Errorf("%w:%s", store.ErrActiveCondition, "pound sand")).Once()
},
mockFleetDB: func(r *fleetdb.MockFleetDB) {
r.On("GetServer", mock.Anything, mock.Anything).
Return(&model.Server{FacilityCode: "facility"}, nil).Once()
},
expectResponse: func() *v1types.ServerResponse {
return &v1types.ServerResponse{
StatusCode: 500,
Message: "server has an active condition",
}
},
expectErrorContains: "",
expectPublish: false,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tester := newTester(t)

if tc.mockStore != nil {
tc.mockStore(tester.repository)
}

if tc.mockFleetDB != nil {
tc.mockFleetDB(tester.fleetDB)
}

if tc.expectPublish {
tester.stream.On(
"Publish",
mock.Anything,
mock.Anything,
mock.Anything,
).Return(nil).Times(1)
}

got, err := tester.client.ServerBiosControl(context.TODO(), tc.payload)
if err != nil {
t.Error(err)
}

if err != nil {
require.Contains(t, err.Error(), tc.expectErrorContains)
}

if tc.expectErrorContains != "" && err == nil {
t.Error("expected error, got nil")
}

require.Equal(t, tc.expectResponse().StatusCode, got.StatusCode, "bad status code")
require.Contains(t, got.Message, tc.expectResponse().Message, "bad message")
})
}
}
60 changes: 60 additions & 0 deletions pkg/api/v1/conditions/routes/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,66 @@ func (r *Routes) firmwareInstallComposite(
return sc
}

// @Summary Bios Control
// @Tag Conditions
// @Description Controls the BIOS of the server
// @Param uuid path string true "Server ID"
// @Param data body rctypes.BiosControlTaskParameters true "bios control options"
// @Accept json
// @Produce json
// @Success 200 {object} v1types.ServerResponse
// Failure 400 {object} v1types.ServerResponse
// Failure 500 {object} v1types.ServerResponse
// Failure 503 {object} v1types.ServerResponse
// @Router /servers/{uuid}/biosControl [post]
func (r *Routes) biosControl(c *gin.Context) (int, *v1types.ServerResponse) {
id := c.Param("uuid")
otelCtx, span := otel.Tracer(pkgName).Start(c.Request.Context(), "Routes.biosControl")
span.SetAttributes(attribute.KeyValue{Key: "serverId", Value: attribute.StringValue(id)})
defer span.End()

serverID, err := uuid.Parse(id)
if err != nil {
r.logger.WithError(err).WithField("serverID", id).Warn("bad serverID")

return http.StatusBadRequest, &v1types.ServerResponse{
Message: "server id: " + err.Error(),
}
}

facilityCode, err := r.serverFacilityCode(otelCtx, serverID)
if err != nil {
return http.StatusInternalServerError, &v1types.ServerResponse{
Message: "server facility: " + err.Error(),
}
}

var bctp rctypes.BiosControlTaskParameters
if err = c.ShouldBindJSON(&bctp); err != nil {
r.logger.WithError(err).Warn("unmarshal biosCotnrol payload")

return http.StatusBadRequest, &v1types.ServerResponse{
Message: "invalid biosCotnrol payload: " + err.Error(),
}
}

createTime := time.Now()
traceID := trace.SpanFromContext(otelCtx).SpanContext().TraceID().String()
spanID := trace.SpanFromContext(otelCtx).SpanContext().SpanID().String()

biosControlCondition := &rctypes.Condition{
Kind: rctypes.BiosControl,
Parameters: bctp.MustJSON(),
State: rctypes.Pending,
CreatedAt: createTime,
TraceID: traceID,
SpanID: spanID,
Client: ginjwt.GetUser(c),
}

return r.conditionCreate(otelCtx, biosControlCondition, serverID, facilityCode)
}

func (r *Routes) conditionCreate(otelCtx context.Context, newCondition *rctypes.Condition, serverID uuid.UUID, facilityCode string) (int, *v1types.ServerResponse) {
// Create the new condition
err := r.repository.Create(otelCtx, serverID, facilityCode, newCondition)
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/v1/conditions/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ func (r *Routes) Routes(g *gin.RouterGroup) {
servers.POST("/firmwareInstall", r.composeAuthHandler(createScopes("condition")),
wrapAPICall(r.firmwareInstall))

// BIOS
servers.POST("/biosControl", r.composeAuthHandler(createScopes("condition")),
wrapAPICall(r.biosControl))

// Generalized API for any condition status (for cases where some server work
// has multiple conditions involved and the caller doesn't know what they might be)
servers.GET("/status", r.composeAuthHandler(readScopes("condition")),
Expand Down

0 comments on commit 34c611c

Please sign in to comment.