diff --git a/cmd/main.go b/cmd/main.go index fc8f5d5b..ffbcb61f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -172,6 +172,7 @@ func runGrpcServer(grpcPort int, useKvm bool, store gokv.Store, spdkAddress, qmp pb.RegisterNvmeRemoteControllerServiceServer(s, backendServer) pb.RegisterNullVolumeServiceServer(s, backendServer) + pb.RegisterMallocVolumeServiceServer(s, backendServer) pb.RegisterAioVolumeServiceServer(s, backendServer) pb.RegisterMiddleendEncryptionServiceServer(s, middleendServer) pb.RegisterMiddleendQosVolumeServiceServer(s, middleendServer) @@ -199,6 +200,7 @@ func runGatewayServer(grpcPort int, httpPort int) { registerGatewayHandler(ctx, mux, endpoint, opts, pb.RegisterAioVolumeServiceHandlerFromEndpoint, "backend aio") registerGatewayHandler(ctx, mux, endpoint, opts, pb.RegisterNullVolumeServiceHandlerFromEndpoint, "backend null") + registerGatewayHandler(ctx, mux, endpoint, opts, pb.RegisterMallocVolumeServiceHandlerFromEndpoint, "backend malloc") registerGatewayHandler(ctx, mux, endpoint, opts, pb.RegisterNvmeRemoteControllerServiceHandlerFromEndpoint, "backend nvme") registerGatewayHandler(ctx, mux, endpoint, opts, pb.RegisterMiddleendEncryptionServiceHandlerFromEndpoint, "middleend encryption") diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go index 723e7d87..d24fa54e 100644 --- a/pkg/backend/backend.go +++ b/pkg/backend/backend.go @@ -20,8 +20,9 @@ import ( // VolumeParameters contains all BackEnd volume related structures type VolumeParameters struct { - AioVolumes map[string]*pb.AioVolume - NullVolumes map[string]*pb.NullVolume + AioVolumes map[string]*pb.AioVolume + NullVolumes map[string]*pb.NullVolume + MallocVolumes map[string]*pb.MallocVolume NvmeControllers map[string]*pb.NvmeRemoteController NvmePaths map[string]*pb.NvmePath @@ -31,6 +32,7 @@ type VolumeParameters struct { type Server struct { pb.UnimplementedNvmeRemoteControllerServiceServer pb.UnimplementedNullVolumeServiceServer + pb.UnimplementedMallocVolumeServiceServer pb.UnimplementedAioVolumeServiceServer rpc spdk.JSONRPC @@ -55,6 +57,7 @@ func NewServer(jsonRPC spdk.JSONRPC, store gokv.Store) *Server { Volumes: VolumeParameters{ AioVolumes: make(map[string]*pb.AioVolume), NullVolumes: make(map[string]*pb.NullVolume), + MallocVolumes: make(map[string]*pb.MallocVolume), NvmeControllers: make(map[string]*pb.NvmeRemoteController), NvmePaths: make(map[string]*pb.NvmePath), }, diff --git a/pkg/backend/backend_test.go b/pkg/backend/backend_test.go index da995185..2fdcfdf2 100644 --- a/pkg/backend/backend_test.go +++ b/pkg/backend/backend_test.go @@ -27,6 +27,8 @@ var checkGlobalTestProtoObjectsNotChanged = utils.CheckTestProtoObjectsNotChange &testAioVolumeWithName, &testNullVolume, &testNullVolumeWithName, + &testMallocVolume, + &testMallocVolumeWithName, &testNvmeCtrl, &testNvmeCtrlWithName, &testNvmePath, @@ -38,6 +40,7 @@ var checkGlobalTestProtoObjectsNotChanged = utils.CheckTestProtoObjectsNotChange type backendClient struct { pb.NvmeRemoteControllerServiceClient pb.NullVolumeServiceClient + pb.MallocVolumeServiceClient pb.AioVolumeServiceClient } @@ -83,6 +86,7 @@ func createTestEnvironment(spdkResponses []string) *testEnv { env.client = &backendClient{ pb.NewNvmeRemoteControllerServiceClient(env.conn), pb.NewNullVolumeServiceClient(env.conn), + pb.NewMallocVolumeServiceClient(env.conn), pb.NewAioVolumeServiceClient(env.conn), } @@ -94,6 +98,7 @@ func dialer(opiSpdkServer *Server) func(context.Context, string) (net.Conn, erro server := grpc.NewServer() pb.RegisterNvmeRemoteControllerServiceServer(server, opiSpdkServer) pb.RegisterNullVolumeServiceServer(server, opiSpdkServer) + pb.RegisterMallocVolumeServiceServer(server, opiSpdkServer) pb.RegisterAioVolumeServiceServer(server, opiSpdkServer) go func() { diff --git a/pkg/backend/malloc.go b/pkg/backend/malloc.go new file mode 100644 index 00000000..6f0d6009 --- /dev/null +++ b/pkg/backend/malloc.go @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2022-2024 Dell Inc, or its subsidiaries. +// Copyright (C) 2023 Intel Corporation + +// Package backend implememnts the BackEnd APIs (network facing) of the storage Server +package backend + +import ( + "context" + "fmt" + "log" + "path" + "sort" + + "github.com/opiproject/gospdk/spdk" + pb "github.com/opiproject/opi-api/storage/v1alpha1/gen/go" + "github.com/opiproject/opi-spdk-bridge/pkg/utils" + + "github.com/google/uuid" + "go.einride.tech/aip/fieldbehavior" + "go.einride.tech/aip/fieldmask" + "go.einride.tech/aip/resourceid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +func sortMallocVolumes(volumes []*pb.MallocVolume) { + sort.Slice(volumes, func(i int, j int) bool { + return volumes[i].Name < volumes[j].Name + }) +} + +// CreateMallocVolume creates a Malloc volume instance +func (s *Server) CreateMallocVolume(ctx context.Context, in *pb.CreateMallocVolumeRequest) (*pb.MallocVolume, error) { + // check input correctness + if err := s.validateCreateMallocVolumeRequest(in); err != nil { + return nil, err + } + // see https://google.aip.dev/133#user-specified-ids + resourceID := resourceid.NewSystemGenerated() + if in.MallocVolumeId != "" { + log.Printf("client provided the ID of a resource %v, ignoring the name field %v", in.MallocVolumeId, in.MallocVolume.Name) + resourceID = in.MallocVolumeId + } + in.MallocVolume.Name = utils.ResourceIDToVolumeName(resourceID) + // idempotent API when called with same key, should return same object + volume, ok := s.Volumes.MallocVolumes[in.MallocVolume.Name] + if ok { + log.Printf("Already existing MallocVolume with id %v", in.MallocVolume.Name) + return volume, nil + } + // not found, so create a new one + params := spdk.BdevMallocCreateParams{ + Name: resourceID, + BlockSize: int(in.GetMallocVolume().GetBlockSize()), + NumBlocks: int(in.GetMallocVolume().GetBlocksCount()), + MdSize: int(in.GetMallocVolume().GetMetadataSize()), + MdInterleave: true, + } + var result spdk.BdevMallocCreateResult + err := s.rpc.Call(ctx, "bdev_malloc_create", ¶ms, &result) + if err != nil { + return nil, err + } + log.Printf("Received from SPDK: %v", result) + if result == "" { + msg := fmt.Sprintf("Could not create Malloc Dev: %s", params.Name) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + response := utils.ProtoClone(in.MallocVolume) + s.Volumes.MallocVolumes[in.MallocVolume.Name] = response + return response, nil +} + +// DeleteMallocVolume deletes a Malloc volume instance +func (s *Server) DeleteMallocVolume(ctx context.Context, in *pb.DeleteMallocVolumeRequest) (*emptypb.Empty, error) { + // check input correctness + if err := s.validateDeleteMallocVolumeRequest(in); err != nil { + return nil, err + } + // fetch object from the database + volume, ok := s.Volumes.MallocVolumes[in.Name] + if !ok { + if in.AllowMissing { + return &emptypb.Empty{}, nil + } + err := status.Errorf(codes.NotFound, "unable to find key %s", in.Name) + return nil, err + } + resourceID := path.Base(volume.Name) + params := spdk.BdevMallocDeleteParams{ + Name: resourceID, + } + var result spdk.BdevMallocDeleteResult + err := s.rpc.Call(ctx, "bdev_malloc_delete", ¶ms, &result) + if err != nil { + return nil, err + } + log.Printf("Received from SPDK: %v", result) + if !result { + msg := fmt.Sprintf("Could not delete Malloc Dev: %s", params.Name) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + delete(s.Volumes.MallocVolumes, volume.Name) + return &emptypb.Empty{}, nil +} + +// UpdateMallocVolume updates a Malloc volume instance +func (s *Server) UpdateMallocVolume(ctx context.Context, in *pb.UpdateMallocVolumeRequest) (*pb.MallocVolume, error) { + // check input correctness + if err := s.validateUpdateMallocVolumeRequest(in); err != nil { + return nil, err + } + // fetch object from the database + volume, ok := s.Volumes.MallocVolumes[in.MallocVolume.Name] + if !ok { + if in.AllowMissing { + log.Printf("Got AllowMissing, create a new resource, don't return error when resource not found") + params := spdk.BdevMallocCreateParams{ + Name: path.Base(in.MallocVolume.Name), + BlockSize: int(in.GetMallocVolume().GetBlockSize()), + NumBlocks: int(in.GetMallocVolume().GetBlocksCount()), + } + var result spdk.BdevMallocCreateResult + err := s.rpc.Call(ctx, "bdev_malloc_create", ¶ms, &result) + if err != nil { + return nil, err + } + log.Printf("Received from SPDK: %v", result) + if result == "" { + msg := fmt.Sprintf("Could not create Malloc Dev: %s", params.Name) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + response := utils.ProtoClone(in.MallocVolume) + s.Volumes.MallocVolumes[in.MallocVolume.Name] = response + return response, nil + } + err := status.Errorf(codes.NotFound, "unable to find key %s", in.MallocVolume.Name) + return nil, err + } + resourceID := path.Base(volume.Name) + // update_mask = 2 + if err := fieldmask.Validate(in.UpdateMask, in.MallocVolume); err != nil { + return nil, err + } + params1 := spdk.BdevMallocDeleteParams{ + Name: resourceID, + } + var result1 spdk.BdevMallocDeleteResult + err1 := s.rpc.Call(ctx, "bdev_malloc_delete", ¶ms1, &result1) + if err1 != nil { + return nil, err1 + } + log.Printf("Received from SPDK: %v", result1) + if !result1 { + msg := fmt.Sprintf("Could not delete Malloc Dev: %s", params1.Name) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + params2 := spdk.BdevMallocCreateParams{ + Name: resourceID, + BlockSize: 512, + NumBlocks: 64, + } + var result2 spdk.BdevMallocCreateResult + err2 := s.rpc.Call(ctx, "bdev_malloc_create", ¶ms2, &result2) + if err2 != nil { + return nil, err2 + } + log.Printf("Received from SPDK: %v", result2) + if result2 == "" { + msg := fmt.Sprintf("Could not create Malloc Dev: %s", params2.Name) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + response := utils.ProtoClone(in.MallocVolume) + s.Volumes.MallocVolumes[in.MallocVolume.Name] = response + return response, nil +} + +// ListMallocVolumes lists Malloc volume instances +func (s *Server) ListMallocVolumes(ctx context.Context, in *pb.ListMallocVolumesRequest) (*pb.ListMallocVolumesResponse, error) { + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return nil, err + } + // fetch object from the database + size, offset, perr := utils.ExtractPagination(in.PageSize, in.PageToken, s.Pagination) + if perr != nil { + return nil, perr + } + var result []spdk.BdevGetBdevsResult + err := s.rpc.Call(ctx, "bdev_get_bdevs", nil, &result) + if err != nil { + return nil, err + } + log.Printf("Received from SPDK: %v", result) + token := "" + log.Printf("Limiting result len(%d) to [%d:%d]", len(result), offset, size) + result, hasMoreElements := utils.LimitPagination(result, offset, size) + if hasMoreElements { + token = uuid.New().String() + s.Pagination[token] = offset + size + } + Blobarray := make([]*pb.MallocVolume, len(result)) + for i := range result { + r := &result[i] + Blobarray[i] = &pb.MallocVolume{Name: r.Name, Uuid: r.UUID, BlockSize: r.BlockSize, BlocksCount: r.NumBlocks} + } + sortMallocVolumes(Blobarray) + return &pb.ListMallocVolumesResponse{MallocVolumes: Blobarray, NextPageToken: token}, nil +} + +// GetMallocVolume gets a a Malloc volume instance +func (s *Server) GetMallocVolume(ctx context.Context, in *pb.GetMallocVolumeRequest) (*pb.MallocVolume, error) { + // check input correctness + if err := s.validateGetMallocVolumeRequest(in); err != nil { + return nil, err + } + // fetch object from the database + volume, ok := s.Volumes.MallocVolumes[in.Name] + if !ok { + err := status.Errorf(codes.NotFound, "unable to find key %s", in.Name) + return nil, err + } + resourceID := path.Base(volume.Name) + params := spdk.BdevGetBdevsParams{ + Name: resourceID, + } + var result []spdk.BdevGetBdevsResult + err := s.rpc.Call(ctx, "bdev_get_bdevs", ¶ms, &result) + if err != nil { + return nil, err + } + log.Printf("Received from SPDK: %v", result) + if len(result) != 1 { + msg := fmt.Sprintf("expecting exactly 1 result, got %d", len(result)) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + return &pb.MallocVolume{Name: result[0].Name, Uuid: result[0].UUID, BlockSize: result[0].BlockSize, BlocksCount: result[0].NumBlocks}, nil +} + +// StatsMallocVolume gets a Malloc volume instance stats +func (s *Server) StatsMallocVolume(ctx context.Context, in *pb.StatsMallocVolumeRequest) (*pb.StatsMallocVolumeResponse, error) { + // check input correctness + if err := s.validateStatsMallocVolumeRequest(in); err != nil { + return nil, err + } + // fetch object from the database + volume, ok := s.Volumes.MallocVolumes[in.Name] + if !ok { + err := status.Errorf(codes.NotFound, "unable to find key %s", in.Name) + return nil, err + } + resourceID := path.Base(volume.Name) + params := spdk.BdevGetIostatParams{ + Name: resourceID, + } + // See https://mholt.github.io/json-to-go/ + var result spdk.BdevGetIostatResult + err := s.rpc.Call(ctx, "bdev_get_iostat", ¶ms, &result) + if err != nil { + return nil, err + } + log.Printf("Received from SPDK: %v", result) + if len(result.Bdevs) != 1 { + msg := fmt.Sprintf("expecting exactly 1 result, got %d", len(result.Bdevs)) + return nil, status.Errorf(codes.InvalidArgument, msg) + } + return &pb.StatsMallocVolumeResponse{Stats: &pb.VolumeStats{ + ReadBytesCount: int32(result.Bdevs[0].BytesRead), + ReadOpsCount: int32(result.Bdevs[0].NumReadOps), + WriteBytesCount: int32(result.Bdevs[0].BytesWritten), + WriteOpsCount: int32(result.Bdevs[0].NumWriteOps), + UnmapBytesCount: int32(result.Bdevs[0].BytesUnmapped), + UnmapOpsCount: int32(result.Bdevs[0].NumUnmapOps), + ReadLatencyTicks: int32(result.Bdevs[0].ReadLatencyTicks), + WriteLatencyTicks: int32(result.Bdevs[0].WriteLatencyTicks), + UnmapLatencyTicks: int32(result.Bdevs[0].UnmapLatencyTicks), + }}, nil +} diff --git a/pkg/backend/malloc_test.go b/pkg/backend/malloc_test.go new file mode 100644 index 00000000..c8e6e350 --- /dev/null +++ b/pkg/backend/malloc_test.go @@ -0,0 +1,844 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2022-2024 Dell Inc, or its subsidiaries. +// Copyright (C) 2023 Intel Corporation + +// Package backend implememnts the BackEnd APIs (network facing) of the storage Server +package backend + +import ( + "fmt" + "reflect" + "testing" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "github.com/opiproject/opi-api/storage/v1alpha1/gen/go" + "github.com/opiproject/opi-spdk-bridge/pkg/utils" +) + +var ( + testMallocVolumeID = "mytest" + testMallocVolumeName = utils.ResourceIDToVolumeName(testMallocVolumeID) + testMallocVolume = pb.MallocVolume{ + BlockSize: 512, + BlocksCount: 64, + } + testMallocVolumeWithName = pb.MallocVolume{ + Name: testMallocVolumeName, + BlockSize: testMallocVolume.BlockSize, + BlocksCount: testMallocVolume.BlocksCount, + } + testRPCHdr = `"jsonrpc":"2.0","id":%d,"result"` + testBdevMalloc0 = `{"name":"Malloc0","aliases":["11d3902e-d9bb-49a7-bb27-cd7261ef3217"],"product_name":"Malloc disk","block_size":512,"num_blocks":131072,"uuid":"11d3902e-d9bb-49a7-bb27-cd7261ef3217","assigned_rate_limits":{"rw_ios_per_sec":0,"rw_mbytes_per_sec":0,"r_mbytes_per_sec":0,"w_mbytes_per_sec":0},"claimed":false,"zoned":false,"supported_io_types":{"read":true,"write":true,"unmap":true,"write_zeroes":true,"flush":true,"reset":true,"compare":false,"compare_and_write":false,"abort":true,"nvme_admin":false,"nvme_io":false},"driver_specific":{}}` + testBdevMalloc1 = `{"name":"Malloc1","aliases":["88112c76-8c49-4395-955a-0d695b1d2099"],"product_name":"Malloc disk","block_size":512,"num_blocks":131072,"uuid":"88112c76-8c49-4395-955a-0d695b1d2099","assigned_rate_limits":{"rw_ios_per_sec":0,"rw_mbytes_per_sec":0,"r_mbytes_per_sec":0,"w_mbytes_per_sec":0},"claimed":false,"zoned":false,"supported_io_types":{"read":true,"write":true,"unmap":true,"write_zeroes":true,"flush":true,"reset":true,"compare":false,"compare_and_write":false,"abort":true,"nvme_admin":false,"nvme_io":false},"driver_specific":{}}` +) + +func TestBackEnd_CreateMallocVolume(t *testing.T) { + t.Cleanup(checkGlobalTestProtoObjectsNotChanged(t, t.Name())) + tests := map[string]struct { + id string + in *pb.MallocVolume + out *pb.MallocVolume + spdk []string + errCode codes.Code + errMsg string + exist bool + }{ + "illegal resource_id": { + id: "CapitalLettersNotAllowed", + in: &testMallocVolume, + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("user-settable ID must only contain lowercase, numbers and hyphens (%v)", "got: 'C' in position 0"), + exist: false, + }, + "valid request with invalid SPDK response": { + id: testMallocVolumeID, + in: &testMallocVolume, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":""}`}, + errCode: codes.InvalidArgument, + errMsg: fmt.Sprintf("Could not create Malloc Dev: %v", testMallocVolumeID), + exist: false, + }, + "valid request with empty SPDK response": { + id: testMallocVolumeID, + in: &testMallocVolume, + out: nil, + spdk: []string{""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_create: %v", "EOF"), + exist: false, + }, + "valid request with ID mismatch SPDK response": { + id: testMallocVolumeID, + in: &testMallocVolume, + out: nil, + spdk: []string{`{"id":0,"error":{"code":0,"message":""},"result":""}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_create: %v", "json response ID mismatch"), + exist: false, + }, + "valid request with error code from SPDK response": { + id: testMallocVolumeID, + in: &testMallocVolume, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":1,"message":"myopierr"},"result":""}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_create: %v", "json response error: myopierr"), + exist: false, + }, + "valid request with valid SPDK response": { + id: testMallocVolumeID, + in: &testMallocVolume, + out: &testMallocVolume, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":"mytest"}`}, + errCode: codes.OK, + errMsg: "", + exist: false, + }, + "already exists": { + id: testMallocVolumeID, + in: &testMallocVolume, + out: &testMallocVolume, + spdk: []string{}, + errCode: codes.OK, + errMsg: "", + exist: true, + }, + "no required field": { + id: testAioVolumeID, + in: nil, + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: "missing required field: malloc_volume", + exist: false, + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testEnv := createTestEnvironment(tt.spdk) + defer testEnv.Close() + + if tt.exist { + testEnv.opiSpdkServer.Volumes.MallocVolumes[testMallocVolumeName] = utils.ProtoClone(&testMallocVolumeWithName) + } + if tt.out != nil { + tt.out = utils.ProtoClone(tt.out) + tt.out.Name = testMallocVolumeName + } + + request := &pb.CreateMallocVolumeRequest{MallocVolume: tt.in, MallocVolumeId: tt.id} + response, err := testEnv.client.CreateMallocVolume(testEnv.ctx, request) + + if !proto.Equal(response, tt.out) { + t.Error("response: expected", tt.out, "received", response) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} + +func TestBackEnd_UpdateMallocVolume(t *testing.T) { + t.Cleanup(checkGlobalTestProtoObjectsNotChanged(t, t.Name())) + + tests := map[string]struct { + mask *fieldmaskpb.FieldMask + in *pb.MallocVolume + out *pb.MallocVolume + spdk []string + errCode codes.Code + errMsg string + missing bool + }{ + "invalid fieldmask": { + mask: &fieldmaskpb.FieldMask{Paths: []string{"*", "author"}}, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("invalid field path: %s", "'*' must not be used with other paths"), + missing: false, + }, + "delete fails": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.InvalidArgument, + errMsg: fmt.Sprintf("Could not delete Malloc Dev: %s", testMallocVolumeID), + missing: false, + }, + "delete empty": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_delete: %v", "EOF"), + missing: false, + }, + "delete ID mismatch": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":0,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_delete: %v", "json response ID mismatch"), + missing: false, + }, + "delete exception": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":1,"message":"myopierr"},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_delete: %v", "json response error: myopierr"), + missing: false, + }, + "delete ok create fails": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":true}`, `{"id":%d,"error":{"code":0,"message":""},"result":""}`}, + errCode: codes.InvalidArgument, + errMsg: fmt.Sprintf("Could not create Malloc Dev: %v", "mytest"), + missing: false, + }, + "delete ok create empty": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":true}`, ""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_create: %v", "EOF"), + missing: false, + }, + "delete ok create ID mismatch": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":true}`, `{"id":0,"error":{"code":0,"message":""},"result":""}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_create: %v", "json response ID mismatch"), + missing: false, + }, + "delete ok create exception": { + mask: nil, + in: &testMallocVolumeWithName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":true}`, `{"id":%d,"error":{"code":1,"message":"myopierr"},"result":""}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_create: %v", "json response error: myopierr"), + missing: false, + }, + "valid request with valid SPDK response": { + mask: nil, + in: &testMallocVolumeWithName, + out: &testMallocVolumeWithName, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":true}`, `{"id":%d,"error":{"code":0,"message":""},"result":"mytest"}`}, + errCode: codes.OK, + errMsg: "", + missing: false, + }, + "valid request with unknown key": { + mask: nil, + in: &pb.MallocVolume{ + Name: utils.ResourceIDToVolumeName("unknown-id"), + BlockSize: 512, + BlocksCount: 64, + }, + out: nil, + spdk: []string{}, + errCode: codes.NotFound, + errMsg: fmt.Sprintf("unable to find key %v", utils.ResourceIDToVolumeName("unknown-id")), + missing: false, + }, + "unknown key with missing allowed": { + mask: nil, + in: &pb.MallocVolume{ + Name: utils.ResourceIDToVolumeName("unknown-id"), + BlockSize: 512, + BlocksCount: 64, + }, + out: &pb.MallocVolume{ + Name: utils.ResourceIDToVolumeName("unknown-id"), + BlockSize: 512, + BlocksCount: 64, + }, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":"mytest"}`}, + errCode: codes.OK, + errMsg: "", + missing: true, + }, + "malformed name": { + mask: nil, + in: &pb.MallocVolume{ + Name: "-ABC-DEF", + BlockSize: testMallocVolume.BlockSize, + BlocksCount: testAioVolume.BlocksCount, + }, + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("segment '%s': not a valid DNS name", "-ABC-DEF"), + missing: false, + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testEnv := createTestEnvironment(tt.spdk) + defer testEnv.Close() + + testEnv.opiSpdkServer.Volumes.MallocVolumes[testMallocVolumeName] = utils.ProtoClone(&testMallocVolumeWithName) + + request := &pb.UpdateMallocVolumeRequest{MallocVolume: tt.in, UpdateMask: tt.mask, AllowMissing: tt.missing} + response, err := testEnv.client.UpdateMallocVolume(testEnv.ctx, request) + + if !proto.Equal(response, tt.out) { + t.Error("response: expected", tt.out, "received", response) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} + +func TestBackEnd_ListMallocVolumes(t *testing.T) { + t.Cleanup(checkGlobalTestProtoObjectsNotChanged(t, t.Name())) + tests := map[string]struct { + in string + out []*pb.MallocVolume + spdk []string + errCode codes.Code + errMsg string + size int32 + token string + }{ + "valid request with empty result SPDK response": { + in: testMallocVolumeID, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":[]}`}, + errCode: codes.OK, + errMsg: "", + size: 0, + token: "", + }, + "valid request with invalid marshal SPDK response": { + in: testMallocVolumeID, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "json: cannot unmarshal bool into Go value of type []spdk.BdevGetBdevsResult"), + size: 0, + token: "", + }, + "valid request with empty SPDK response": { + in: testMallocVolumeID, + out: nil, + spdk: []string{""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "EOF"), + size: 0, + token: "", + }, + "valid request with ID mismatch SPDK response": { + in: testMallocVolumeID, + out: nil, + spdk: []string{`{"id":0,"error":{"code":0,"message":""},"result":[]}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "json response ID mismatch"), + size: 0, + token: "", + }, + "valid request with error code from SPDK response": { + in: testMallocVolumeID, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":1,"message":"myopierr"}}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "json response error: myopierr"), + size: 0, + token: "", + }, + "valid request with valid SPDK response": { + in: testMallocVolumeID, + out: []*pb.MallocVolume{ + { + Name: "Malloc0", + Uuid: "11d3902e-d9bb-49a7-bb27-cd7261ef3217", + BlockSize: 512, + BlocksCount: 131072, + }, + { + Name: "Malloc1", + Uuid: "88112c76-8c49-4395-955a-0d695b1d2099", + BlockSize: 512, + BlocksCount: 131072, + }, + }, + spdk: []string{`{` + testRPCHdr + `:[` + testBdevMalloc1 + `,` + testBdevMalloc0 + `]}`}, + errCode: codes.OK, + errMsg: "", + size: 0, + token: "", + }, + "pagination overflow": { + in: testMallocVolumeID, + out: []*pb.MallocVolume{ + { + Name: "Malloc0", + Uuid: "11d3902e-d9bb-49a7-bb27-cd7261ef3217", + BlockSize: 512, + BlocksCount: 131072, + }, + { + Name: "Malloc1", + Uuid: "88112c76-8c49-4395-955a-0d695b1d2099", + BlockSize: 512, + BlocksCount: 131072, + }, + }, + spdk: []string{`{` + testRPCHdr + `:[` + testBdevMalloc0 + `,` + testBdevMalloc1 + `]}`}, + errCode: codes.OK, + errMsg: "", + size: 1000, + token: "", + }, + "pagination negative": { + in: testMallocVolumeID, + out: nil, + spdk: []string{}, + errCode: codes.InvalidArgument, + errMsg: "negative PageSize is not allowed", + size: -10, + token: "", + }, + "pagination error": { + in: testMallocVolumeID, + out: nil, + spdk: []string{}, + errCode: codes.NotFound, + errMsg: fmt.Sprintf("unable to find pagination token %s", "unknown-pagination-token"), + size: 0, + token: "unknown-pagination-token", + }, + "pagination": { + in: testMallocVolumeID, + out: []*pb.MallocVolume{ + { + Name: "Malloc0", + Uuid: "11d3902e-d9bb-49a7-bb27-cd7261ef3217", + BlockSize: 512, + BlocksCount: 131072, + }, + }, + spdk: []string{`{` + testRPCHdr + `:[` + testBdevMalloc0 + `,` + testBdevMalloc1 + `]}`}, + errCode: codes.OK, + errMsg: "", + size: 1, + token: "", + }, + "pagination offset": { + in: testMallocVolumeID, + out: []*pb.MallocVolume{ + { + Name: "Malloc1", + Uuid: "88112c76-8c49-4395-955a-0d695b1d2099", + BlockSize: 512, + BlocksCount: 131072, + }, + }, + spdk: []string{`{` + testRPCHdr + `:[` + testBdevMalloc0 + `,` + testBdevMalloc1 + `]}`}, + errCode: codes.OK, + errMsg: "", + size: 1, + token: "existing-pagination-token", + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testEnv := createTestEnvironment(tt.spdk) + defer testEnv.Close() + + testEnv.opiSpdkServer.Pagination["existing-pagination-token"] = 1 + + request := &pb.ListMallocVolumesRequest{PageSize: tt.size, PageToken: tt.token} + response, err := testEnv.client.ListMallocVolumes(testEnv.ctx, request) + + if !utils.EqualProtoSlices(response.GetMallocVolumes(), tt.out) { + t.Error("response: expected", tt.out, "received", response.GetMallocVolumes()) + } + + if tt.size != 1 && response.GetNextPageToken() != "" { + t.Error("Expected end of results, received non-empty next page token", response.GetNextPageToken()) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} + +func TestBackEnd_GetMallocVolume(t *testing.T) { + t.Cleanup(checkGlobalTestProtoObjectsNotChanged(t, t.Name())) + tests := map[string]struct { + in string + out *pb.MallocVolume + spdk []string + errCode codes.Code + errMsg string + }{ + "valid request with invalid SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":[]}`}, + errCode: codes.InvalidArgument, + errMsg: fmt.Sprintf("expecting exactly 1 result, got %v", "0"), + }, + "valid request with invalid marshal SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "json: cannot unmarshal bool into Go value of type []spdk.BdevGetBdevsResult"), + }, + "valid request with empty SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "EOF"), + }, + "valid request with ID mismatch SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":0,"error":{"code":0,"message":""},"result":[]}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "json response ID mismatch"), + }, + "valid request with error code from SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":1,"message":"myopierr"}}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_bdevs: %v", "json response error: myopierr"), + }, + "valid request with valid SPDK response": { + in: testMallocVolumeName, + out: &pb.MallocVolume{ + Name: "Malloc1", + Uuid: "88112c76-8c49-4395-955a-0d695b1d2099", + BlockSize: 512, + BlocksCount: 131072, + }, + spdk: []string{`{` + testRPCHdr + `:[` + testBdevMalloc1 + `]}`}, + errCode: codes.OK, + errMsg: "", + }, + "valid request with unknown key": { + in: "unknown-id", + out: nil, + spdk: []string{}, + errCode: codes.NotFound, + errMsg: fmt.Sprintf("unable to find key %v", "unknown-id"), + }, + "malformed name": { + in: "-ABC-DEF", + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("segment '%s': not a valid DNS name", "-ABC-DEF"), + }, + "no required field": { + in: "", + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: "missing required field: name", + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testEnv := createTestEnvironment(tt.spdk) + defer testEnv.Close() + + testEnv.opiSpdkServer.Volumes.MallocVolumes[testMallocVolumeName] = utils.ProtoClone(&testMallocVolumeWithName) + + request := &pb.GetMallocVolumeRequest{Name: tt.in} + response, err := testEnv.client.GetMallocVolume(testEnv.ctx, request) + + if !proto.Equal(response, tt.out) { + t.Error("response: expected", tt.out, "received", response) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} + +func TestBackEnd_StatsMallocVolume(t *testing.T) { + t.Cleanup(checkGlobalTestProtoObjectsNotChanged(t, t.Name())) + tests := map[string]struct { + in string + out *pb.VolumeStats + spdk []string + errCode codes.Code + errMsg string + }{ + "valid request with invalid SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":{"tick_rate":0,"ticks":0,"bdevs":null}}`}, + errCode: codes.InvalidArgument, + errMsg: fmt.Sprintf("expecting exactly 1 result, got %v", "0"), + }, + "valid request with invalid marshal SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_iostat: %v", "json: cannot unmarshal bool into Go value of type spdk.BdevGetIostatResult"), + }, + "valid request with empty SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_iostat: %v", "EOF"), + }, + "valid request with ID mismatch SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":0,"error":{"code":0,"message":""},"result":{"tick_rate":0,"ticks":0,"bdevs":null}}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_iostat: %v", "json response ID mismatch"), + }, + "valid request with error code from SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":1,"message":"myopierr"}}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_get_iostat: %v", "json response error: myopierr"), + }, + "valid request with valid SPDK response": { + in: testMallocVolumeName, + out: &pb.VolumeStats{ + ReadBytesCount: 1, + ReadOpsCount: 2, + WriteBytesCount: 3, + WriteOpsCount: 4, + ReadLatencyTicks: 7, + WriteLatencyTicks: 8, + }, + spdk: []string{`{"jsonrpc":"2.0","id":%d,"result":{"tick_rate":2490000000,"ticks":18787040917434338,"bdevs":[{"name":"mytest","bytes_read":1,"num_read_ops":2,"bytes_written":3,"num_write_ops":4,"bytes_unmapped":0,"num_unmap_ops":0,"read_latency_ticks":7,"write_latency_ticks":8,"unmap_latency_ticks":0}]}}`}, + errCode: codes.OK, + errMsg: "", + }, + "valid request with unknown key": { + in: "unknown-id", + out: nil, + spdk: []string{}, + errCode: codes.NotFound, + errMsg: fmt.Sprintf("unable to find key %v", "unknown-id"), + }, + "malformed name": { + in: "-ABC-DEF", + out: nil, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("segment '%s': not a valid DNS name", "-ABC-DEF"), + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testEnv := createTestEnvironment(tt.spdk) + defer testEnv.Close() + + testEnv.opiSpdkServer.Volumes.MallocVolumes[testMallocVolumeName] = utils.ProtoClone(&testMallocVolumeWithName) + + request := &pb.StatsMallocVolumeRequest{Name: tt.in} + response, err := testEnv.client.StatsMallocVolume(testEnv.ctx, request) + + if !proto.Equal(response.GetStats(), tt.out) { + t.Error("response: expected", tt.out, "received", response.GetStats()) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} + +func TestBackEnd_DeleteMallocVolume(t *testing.T) { + t.Cleanup(checkGlobalTestProtoObjectsNotChanged(t, t.Name())) + tests := map[string]struct { + in string + out *emptypb.Empty + spdk []string + errCode codes.Code + errMsg string + missing bool + }{ + "valid request with invalid SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.InvalidArgument, + errMsg: fmt.Sprintf("Could not delete Malloc Dev: %s", testMallocVolumeID), + missing: false, + }, + "valid request with empty SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{""}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_delete: %v", "EOF"), + missing: false, + }, + "valid request with ID mismatch SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":0,"error":{"code":0,"message":""},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_delete: %v", "json response ID mismatch"), + missing: false, + }, + "valid request with error code from SPDK response": { + in: testMallocVolumeName, + out: nil, + spdk: []string{`{"id":%d,"error":{"code":1,"message":"myopierr"},"result":false}`}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("bdev_malloc_delete: %v", "json response error: myopierr"), + missing: false, + }, + "valid request with valid SPDK response": { + in: testMallocVolumeName, + out: &emptypb.Empty{}, + spdk: []string{`{"id":%d,"error":{"code":0,"message":""},"result":true}`}, // `{"jsonrpc": "2.0", "id": 1, "result": True}`, + errCode: codes.OK, + errMsg: "", + missing: false, + }, + "valid request with unknown key": { + in: utils.ResourceIDToVolumeName("unknown-id"), + out: nil, + spdk: []string{}, + errCode: codes.NotFound, + errMsg: fmt.Sprintf("unable to find key %v", utils.ResourceIDToVolumeName("unknown-id")), + missing: false, + }, + "unknown key with missing allowed": { + in: utils.ResourceIDToVolumeName("unknown-id"), + out: &emptypb.Empty{}, + spdk: []string{}, + errCode: codes.OK, + errMsg: "", + missing: true, + }, + "malformed name": { + in: utils.ResourceIDToVolumeName("-ABC-DEF"), + out: &emptypb.Empty{}, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: fmt.Sprintf("segment '%s': not a valid DNS name", "-ABC-DEF"), + missing: false, + }, + "no required field": { + in: "", + out: &emptypb.Empty{}, + spdk: []string{}, + errCode: codes.Unknown, + errMsg: "missing required field: name", + missing: false, + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testEnv := createTestEnvironment(tt.spdk) + defer testEnv.Close() + + testEnv.opiSpdkServer.Volumes.MallocVolumes[testMallocVolumeName] = utils.ProtoClone(&testMallocVolumeWithName) + + request := &pb.DeleteMallocVolumeRequest{Name: tt.in, AllowMissing: tt.missing} + response, err := testEnv.client.DeleteMallocVolume(testEnv.ctx, request) + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + + if reflect.TypeOf(response) != reflect.TypeOf(tt.out) { + t.Error("response: expected", reflect.TypeOf(tt.out), "received", reflect.TypeOf(response)) + } + }) + } +} diff --git a/pkg/backend/malloc_validate.go b/pkg/backend/malloc_validate.go new file mode 100644 index 00000000..88ca77d1 --- /dev/null +++ b/pkg/backend/malloc_validate.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2022-2023 Dell Inc, or its subsidiaries. + +// Package backend implememnts the BackEnd APIs (network facing) of the storage Server +package backend + +import ( + "go.einride.tech/aip/fieldbehavior" + "go.einride.tech/aip/resourceid" + "go.einride.tech/aip/resourcename" + + pb "github.com/opiproject/opi-api/storage/v1alpha1/gen/go" +) + +func (s *Server) validateCreateMallocVolumeRequest(in *pb.CreateMallocVolumeRequest) error { + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return err + } + // see https://google.aip.dev/133#user-specified-ids + if in.MallocVolumeId != "" { + if err := resourceid.ValidateUserSettable(in.MallocVolumeId); err != nil { + return err + } + } + // TODO: validate also: block_size, blocks_count, md_size, uuid + return nil +} + +func (s *Server) validateDeleteMallocVolumeRequest(in *pb.DeleteMallocVolumeRequest) error { + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return err + } + // Validate that a resource name conforms to the restrictions outlined in AIP-122. + return resourcename.Validate(in.Name) +} + +func (s *Server) validateUpdateMallocVolumeRequest(in *pb.UpdateMallocVolumeRequest) error { + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return err + } + // Validate that a resource name conforms to the restrictions outlined in AIP-122. + return resourcename.Validate(in.MallocVolume.Name) +} + +func (s *Server) validateGetMallocVolumeRequest(in *pb.GetMallocVolumeRequest) error { + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return err + } + // Validate that a resource name conforms to the restrictions outlined in AIP-122. + return resourcename.Validate(in.Name) +} + +func (s *Server) validateStatsMallocVolumeRequest(in *pb.StatsMallocVolumeRequest) error { + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return err + } + // Validate that a resource name conforms to the restrictions outlined in AIP-122. + return resourcename.Validate(in.Name) +}