From dca7e009fa83f93efac4fe1bd22ead15f1d13837 Mon Sep 17 00:00:00 2001 From: Masoud Mazarei Date: Sat, 7 Nov 2026 12:26:24 -0500 Subject: [PATCH 1/3] feat(gateway): add phase 4 kouja storage routing Wire the kouja driver into disk create/delete, backend config mapping and selection, VM start planning, autostart, deletion reclamation, and backend health. Disk deletes now return 202 for queued remote cleanup. --- cmd/rookery/serve.go | 9 + design/gen/rookery/v1/disk.pb.go | 106 +++- design/gen/rookery/v1/disk.pb.validate.go | 106 ++++ design/gen/rookery/v1/disk_grpc.pb.go | 11 +- design/gen/rookery/v1/storage_backend.pb.go | 443 +++++++++++++-- .../gen/rookery/v1/storage_backend.pb.gw.go | 190 ++++++- .../rookery/v1/storage_backend.pb.validate.go | 529 ++++++++++++++++++ .../gen/rookery/v1/storage_backend_grpc.pb.go | 90 ++- design/openapi/rookery.swagger.json | 169 +++++- design/proto/rookery/v1/disk.proto | 8 +- design/proto/rookery/v1/storage_backend.proto | 44 ++ internal/api/api_test.go | 8 +- internal/api/reconcile.go | 19 +- internal/api/server.go | 11 + internal/gateway/disk_kouja.go | 99 ++++ internal/gateway/disk_kouja_test.go | 152 +++++ internal/gateway/disk_writes.go | 29 +- internal/gateway/gateway.go | 32 +- internal/gateway/http_status.go | 10 +- internal/gateway/readonly.go | 34 +- internal/gateway/storage_backend_health.go | 84 +++ internal/gateway/vm_actions.go | 20 +- internal/gateway/vm_actions_test.go | 5 + internal/gateway/vm_create.go | 2 +- internal/gateway/writes.go | 19 + 25 files changed, 2104 insertions(+), 125 deletions(-) create mode 100644 internal/gateway/disk_kouja.go create mode 100644 internal/gateway/disk_kouja_test.go create mode 100644 internal/gateway/storage_backend_health.go diff --git a/cmd/rookery/serve.go b/cmd/rookery/serve.go index 2084439..afb4264 100644 --- a/cmd/rookery/serve.go +++ b/cmd/rookery/serve.go @@ -28,7 +28,9 @@ import ( "github.com/voidstarit/rookery/internal/pki" "github.com/voidstarit/rookery/internal/qemu" "github.com/voidstarit/rookery/internal/readiness" + "github.com/voidstarit/rookery/internal/storage" "github.com/voidstarit/rookery/internal/store" + "github.com/voidstarit/rookery/internal/vmstart" "github.com/voidstarit/rookery/internal/vnfd" ) @@ -173,7 +175,11 @@ func runServe(args []string) error { ) diskRunner := host.NewLVM() + storageRegistry := storage.NewDefaultRegistry() + startPlanner := vmstart.NewPlanner(st, storageRegistry) + diskReclaimer := storage.NewReclaimer(st, storageRegistry, logger) apiServer := api.New(st, manager, diskRunner, qemuRunner, networkRunner, vnfClient, networkRunner, logger) + apiServer.ConfigureStartPlanner(startPlanner) lightvmStorage, err := lightvm.NewStorage(*lightvmDir, lightvm.DefaultMaxUploadBytes) if err != nil { return fmt.Errorf("prepare LightVM storage: %w", err) @@ -209,6 +215,8 @@ func runServe(args []string) error { gateway.WithInterfaceCounters(networkRunner), gateway.WithACLRuntime(networkRunner), gateway.WithDiskProvisioner(diskRunner), + gateway.WithStorageRegistry(storageRegistry), + gateway.WithStartPlanner(startPlanner), gateway.WithDiskMutex(apiServer.ResourceMutex()), gateway.WithVMMutex(apiServer.RunMutex()), gateway.WithInterfaceRuntime(networkRunner), @@ -298,6 +306,7 @@ func runServe(args []string) error { defer stop() go manager.Run(ctx) + go diskReclaimer.Run(ctx) go apiServer.RunInfrastructureReconciler(ctx) servers := []*http.Server{srv} diff --git a/design/gen/rookery/v1/disk.pb.go b/design/gen/rookery/v1/disk.pb.go index b03e8bb..305f331 100644 --- a/design/gen/rookery/v1/disk.pb.go +++ b/design/gen/rookery/v1/disk.pb.go @@ -11,7 +11,6 @@ import ( _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - emptypb "google.golang.org/protobuf/types/known/emptypb" structpb "google.golang.org/protobuf/types/known/structpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" @@ -716,6 +715,58 @@ func (x *CreateDiskRequest) GetHostDevice() string { return "" } +type DeleteDiskResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteDiskResponse) Reset() { + *x = DeleteDiskResponse{} + mi := &file_rookery_v1_disk_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteDiskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteDiskResponse) ProtoMessage() {} + +func (x *DeleteDiskResponse) ProtoReflect() protoreflect.Message { + mi := &file_rookery_v1_disk_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteDiskResponse.ProtoReflect.Descriptor instead. +func (*DeleteDiskResponse) Descriptor() ([]byte, []int) { + return file_rookery_v1_disk_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteDiskResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +func (x *DeleteDiskResponse) GetState() string { + if x != nil { + return x.State + } + return "" +} + type UpdateDiskRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` @@ -728,7 +779,7 @@ type UpdateDiskRequest struct { func (x *UpdateDiskRequest) Reset() { *x = UpdateDiskRequest{} - mi := &file_rookery_v1_disk_proto_msgTypes[7] + mi := &file_rookery_v1_disk_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -740,7 +791,7 @@ func (x *UpdateDiskRequest) String() string { func (*UpdateDiskRequest) ProtoMessage() {} func (x *UpdateDiskRequest) ProtoReflect() protoreflect.Message { - mi := &file_rookery_v1_disk_proto_msgTypes[7] + mi := &file_rookery_v1_disk_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -753,7 +804,7 @@ func (x *UpdateDiskRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateDiskRequest.ProtoReflect.Descriptor instead. func (*UpdateDiskRequest) Descriptor() ([]byte, []int) { - return file_rookery_v1_disk_proto_rawDescGZIP(), []int{7} + return file_rookery_v1_disk_proto_rawDescGZIP(), []int{8} } func (x *UpdateDiskRequest) GetId() int64 { @@ -793,7 +844,7 @@ type DeleteDiskRequest struct { func (x *DeleteDiskRequest) Reset() { *x = DeleteDiskRequest{} - mi := &file_rookery_v1_disk_proto_msgTypes[8] + mi := &file_rookery_v1_disk_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -805,7 +856,7 @@ func (x *DeleteDiskRequest) String() string { func (*DeleteDiskRequest) ProtoMessage() {} func (x *DeleteDiskRequest) ProtoReflect() protoreflect.Message { - mi := &file_rookery_v1_disk_proto_msgTypes[8] + mi := &file_rookery_v1_disk_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -818,7 +869,7 @@ func (x *DeleteDiskRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteDiskRequest.ProtoReflect.Descriptor instead. func (*DeleteDiskRequest) Descriptor() ([]byte, []int) { - return file_rookery_v1_disk_proto_rawDescGZIP(), []int{8} + return file_rookery_v1_disk_proto_rawDescGZIP(), []int{9} } func (x *DeleteDiskRequest) GetId() int64 { @@ -833,7 +884,7 @@ var File_rookery_v1_disk_proto protoreflect.FileDescriptor const file_rookery_v1_disk_proto_rawDesc = "" + "\n" + "\x15rookery/v1/disk.proto\x12\n" + - "rookery.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17validate/validate.proto\"\xf3\x02\n" + + "rookery.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17validate/validate.proto\"\xf3\x02\n" + "\x06VMDisk\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x17\n" + @@ -917,7 +968,10 @@ const file_rookery_v1_disk_proto_rawDesc = "" + "\b_size_gbB\x15\n" + "\x13_storage_backend_idB\x13\n" + "\x11_template_disk_idB\x0e\n" + - "\f_is_template\"\x94\x02\n" + + "\f_is_template\"F\n" + + "\x12DeleteDiskResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\"\x94\x02\n" + "\x11UpdateDiskRequest\x12\x17\n" + "\x02id\x18\x01 \x01(\x03B\a\xfaB\x04\"\x02 \x00R\x02id\x12$\n" + "\vis_template\x18\x02 \x01(\bH\x00R\n" + @@ -928,7 +982,7 @@ const file_rookery_v1_disk_proto_rawDesc = "" + "\x14_provisioning_scriptB\x17\n" + "\x15_provisioning_timeout\",\n" + "\x11DeleteDiskRequest\x12\x17\n" + - "\x02id\x18\x01 \x01(\x03B\a\xfaB\x04\"\x02 \x00R\x02id2\xd3\x03\n" + + "\x02id\x18\x01 \x01(\x03B\a\xfaB\x04\"\x02 \x00R\x02id2\xdb\x03\n" + "\vDiskService\x12c\n" + "\tListDisks\x12\x1c.rookery.v1.ListDisksRequest\x1a\x1d.rookery.v1.ListDisksResponse\"\x19\x82\xd3\xe4\x93\x02\x13b\x05disks\x12\n" + "/api/disks\x12P\n" + @@ -937,9 +991,9 @@ const file_rookery_v1_disk_proto_rawDesc = "" + "CreateDisk\x12\x1d.rookery.v1.CreateDiskRequest\x1a\x10.rookery.v1.Disk\"\x15\x82\xd3\xe4\x93\x02\x0f:\x01*\"\n" + "/api/disks\x12Y\n" + "\n" + - "UpdateDisk\x12\x1d.rookery.v1.UpdateDiskRequest\x1a\x10.rookery.v1.Disk\"\x1a\x82\xd3\xe4\x93\x02\x14:\x01*\x1a\x0f/api/disks/{id}\x12\\\n" + + "UpdateDisk\x12\x1d.rookery.v1.UpdateDiskRequest\x1a\x10.rookery.v1.Disk\"\x1a\x82\xd3\xe4\x93\x02\x14:\x01*\x1a\x0f/api/disks/{id}\x12d\n" + "\n" + - "DeleteDisk\x12\x1d.rookery.v1.DeleteDiskRequest\x1a\x16.google.protobuf.Empty\"\x17\x82\xd3\xe4\x93\x02\x11*\x0f/api/disks/{id}B?Z=github.com/voidstarit/rookery/design/gen/rookery/v1;rookeryv1b\x06proto3" + "DeleteDisk\x12\x1d.rookery.v1.DeleteDiskRequest\x1a\x1e.rookery.v1.DeleteDiskResponse\"\x17\x82\xd3\xe4\x93\x02\x11*\x0f/api/disks/{id}B?Z=github.com/voidstarit/rookery/design/gen/rookery/v1;rookeryv1b\x06proto3" var ( file_rookery_v1_disk_proto_rawDescOnce sync.Once @@ -953,7 +1007,7 @@ func file_rookery_v1_disk_proto_rawDescGZIP() []byte { return file_rookery_v1_disk_proto_rawDescData } -var file_rookery_v1_disk_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_rookery_v1_disk_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_rookery_v1_disk_proto_goTypes = []any{ (*VMDisk)(nil), // 0: rookery.v1.VMDisk (*Disk)(nil), // 1: rookery.v1.Disk @@ -962,27 +1016,27 @@ var file_rookery_v1_disk_proto_goTypes = []any{ (*ListDisksResponse)(nil), // 4: rookery.v1.ListDisksResponse (*GetDiskRequest)(nil), // 5: rookery.v1.GetDiskRequest (*CreateDiskRequest)(nil), // 6: rookery.v1.CreateDiskRequest - (*UpdateDiskRequest)(nil), // 7: rookery.v1.UpdateDiskRequest - (*DeleteDiskRequest)(nil), // 8: rookery.v1.DeleteDiskRequest - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 10: google.protobuf.Struct - (*emptypb.Empty)(nil), // 11: google.protobuf.Empty + (*DeleteDiskResponse)(nil), // 7: rookery.v1.DeleteDiskResponse + (*UpdateDiskRequest)(nil), // 8: rookery.v1.UpdateDiskRequest + (*DeleteDiskRequest)(nil), // 9: rookery.v1.DeleteDiskRequest + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 11: google.protobuf.Struct } var file_rookery_v1_disk_proto_depIdxs = []int32{ - 9, // 0: rookery.v1.Disk.created_at:type_name -> google.protobuf.Timestamp - 9, // 1: rookery.v1.Disk.updated_at:type_name -> google.protobuf.Timestamp + 10, // 0: rookery.v1.Disk.created_at:type_name -> google.protobuf.Timestamp + 10, // 1: rookery.v1.Disk.updated_at:type_name -> google.protobuf.Timestamp 1, // 2: rookery.v1.ListDisksResponse.disks:type_name -> rookery.v1.Disk - 10, // 3: rookery.v1.CreateDiskRequest.provisioning:type_name -> google.protobuf.Struct + 11, // 3: rookery.v1.CreateDiskRequest.provisioning:type_name -> google.protobuf.Struct 3, // 4: rookery.v1.DiskService.ListDisks:input_type -> rookery.v1.ListDisksRequest 5, // 5: rookery.v1.DiskService.GetDisk:input_type -> rookery.v1.GetDiskRequest 6, // 6: rookery.v1.DiskService.CreateDisk:input_type -> rookery.v1.CreateDiskRequest - 7, // 7: rookery.v1.DiskService.UpdateDisk:input_type -> rookery.v1.UpdateDiskRequest - 8, // 8: rookery.v1.DiskService.DeleteDisk:input_type -> rookery.v1.DeleteDiskRequest + 8, // 7: rookery.v1.DiskService.UpdateDisk:input_type -> rookery.v1.UpdateDiskRequest + 9, // 8: rookery.v1.DiskService.DeleteDisk:input_type -> rookery.v1.DeleteDiskRequest 4, // 9: rookery.v1.DiskService.ListDisks:output_type -> rookery.v1.ListDisksResponse 1, // 10: rookery.v1.DiskService.GetDisk:output_type -> rookery.v1.Disk 1, // 11: rookery.v1.DiskService.CreateDisk:output_type -> rookery.v1.Disk 1, // 12: rookery.v1.DiskService.UpdateDisk:output_type -> rookery.v1.Disk - 11, // 13: rookery.v1.DiskService.DeleteDisk:output_type -> google.protobuf.Empty + 7, // 13: rookery.v1.DiskService.DeleteDisk:output_type -> rookery.v1.DeleteDiskResponse 9, // [9:14] is the sub-list for method output_type 4, // [4:9] is the sub-list for method input_type 4, // [4:4] is the sub-list for extension type_name @@ -999,14 +1053,14 @@ func file_rookery_v1_disk_proto_init() { file_rookery_v1_disk_proto_msgTypes[1].OneofWrappers = []any{} file_rookery_v1_disk_proto_msgTypes[2].OneofWrappers = []any{} file_rookery_v1_disk_proto_msgTypes[6].OneofWrappers = []any{} - file_rookery_v1_disk_proto_msgTypes[7].OneofWrappers = []any{} + file_rookery_v1_disk_proto_msgTypes[8].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_rookery_v1_disk_proto_rawDesc), len(file_rookery_v1_disk_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/design/gen/rookery/v1/disk.pb.validate.go b/design/gen/rookery/v1/disk.pb.validate.go index f0458c0..a6ea034 100644 --- a/design/gen/rookery/v1/disk.pb.validate.go +++ b/design/gen/rookery/v1/disk.pb.validate.go @@ -1077,6 +1077,112 @@ var _ interface { var _CreateDiskRequest_Name_Pattern = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9._-]*$") +// Validate checks the field values on DeleteDiskResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *DeleteDiskResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on DeleteDiskResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// DeleteDiskResponseMultiError, or nil if none found. +func (m *DeleteDiskResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *DeleteDiskResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Accepted + + // no validation rules for State + + if len(errors) > 0 { + return DeleteDiskResponseMultiError(errors) + } + + return nil +} + +// DeleteDiskResponseMultiError is an error wrapping multiple validation errors +// returned by DeleteDiskResponse.ValidateAll() if the designated constraints +// aren't met. +type DeleteDiskResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m DeleteDiskResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m DeleteDiskResponseMultiError) AllErrors() []error { return m } + +// DeleteDiskResponseValidationError is the validation error returned by +// DeleteDiskResponse.Validate if the designated constraints aren't met. +type DeleteDiskResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e DeleteDiskResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e DeleteDiskResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e DeleteDiskResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e DeleteDiskResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e DeleteDiskResponseValidationError) ErrorName() string { + return "DeleteDiskResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e DeleteDiskResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sDeleteDiskResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = DeleteDiskResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = DeleteDiskResponseValidationError{} + // Validate checks the field values on UpdateDiskRequest with the rules defined // in the proto definition for this message. If any rules are violated, the // first error encountered is returned, or nil if there are no violations. diff --git a/design/gen/rookery/v1/disk_grpc.pb.go b/design/gen/rookery/v1/disk_grpc.pb.go index 212abb1..dac5b3c 100644 --- a/design/gen/rookery/v1/disk_grpc.pb.go +++ b/design/gen/rookery/v1/disk_grpc.pb.go @@ -11,7 +11,6 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" - emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file @@ -37,7 +36,7 @@ type DiskServiceClient interface { GetDisk(ctx context.Context, in *GetDiskRequest, opts ...grpc.CallOption) (*Disk, error) CreateDisk(ctx context.Context, in *CreateDiskRequest, opts ...grpc.CallOption) (*Disk, error) UpdateDisk(ctx context.Context, in *UpdateDiskRequest, opts ...grpc.CallOption) (*Disk, error) - DeleteDisk(ctx context.Context, in *DeleteDiskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteDisk(ctx context.Context, in *DeleteDiskRequest, opts ...grpc.CallOption) (*DeleteDiskResponse, error) } type diskServiceClient struct { @@ -88,9 +87,9 @@ func (c *diskServiceClient) UpdateDisk(ctx context.Context, in *UpdateDiskReques return out, nil } -func (c *diskServiceClient) DeleteDisk(ctx context.Context, in *DeleteDiskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *diskServiceClient) DeleteDisk(ctx context.Context, in *DeleteDiskRequest, opts ...grpc.CallOption) (*DeleteDiskResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) + out := new(DeleteDiskResponse) err := c.cc.Invoke(ctx, DiskService_DeleteDisk_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -108,7 +107,7 @@ type DiskServiceServer interface { GetDisk(context.Context, *GetDiskRequest) (*Disk, error) CreateDisk(context.Context, *CreateDiskRequest) (*Disk, error) UpdateDisk(context.Context, *UpdateDiskRequest) (*Disk, error) - DeleteDisk(context.Context, *DeleteDiskRequest) (*emptypb.Empty, error) + DeleteDisk(context.Context, *DeleteDiskRequest) (*DeleteDiskResponse, error) mustEmbedUnimplementedDiskServiceServer() } @@ -131,7 +130,7 @@ func (UnimplementedDiskServiceServer) CreateDisk(context.Context, *CreateDiskReq func (UnimplementedDiskServiceServer) UpdateDisk(context.Context, *UpdateDiskRequest) (*Disk, error) { return nil, status.Error(codes.Unimplemented, "method UpdateDisk not implemented") } -func (UnimplementedDiskServiceServer) DeleteDisk(context.Context, *DeleteDiskRequest) (*emptypb.Empty, error) { +func (UnimplementedDiskServiceServer) DeleteDisk(context.Context, *DeleteDiskRequest) (*DeleteDiskResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeleteDisk not implemented") } func (UnimplementedDiskServiceServer) mustEmbedUnimplementedDiskServiceServer() {} diff --git a/design/gen/rookery/v1/storage_backend.pb.go b/design/gen/rookery/v1/storage_backend.pb.go index a6eb504..5aa7a36 100644 --- a/design/gen/rookery/v1/storage_backend.pb.go +++ b/design/gen/rookery/v1/storage_backend.pb.go @@ -876,6 +876,318 @@ func (x *ImportDisksResponse) GetDisks() []*Disk { return nil } +type GetStorageBackendHealthRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStorageBackendHealthRequest) Reset() { + *x = GetStorageBackendHealthRequest{} + mi := &file_rookery_v1_storage_backend_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStorageBackendHealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStorageBackendHealthRequest) ProtoMessage() {} + +func (x *GetStorageBackendHealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_rookery_v1_storage_backend_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStorageBackendHealthRequest.ProtoReflect.Descriptor instead. +func (*GetStorageBackendHealthRequest) Descriptor() ([]byte, []int) { + return file_rookery_v1_storage_backend_proto_rawDescGZIP(), []int{13} +} + +func (x *GetStorageBackendHealthRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type TestStorageBackendDataPathRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestStorageBackendDataPathRequest) Reset() { + *x = TestStorageBackendDataPathRequest{} + mi := &file_rookery_v1_storage_backend_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestStorageBackendDataPathRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestStorageBackendDataPathRequest) ProtoMessage() {} + +func (x *TestStorageBackendDataPathRequest) ProtoReflect() protoreflect.Message { + mi := &file_rookery_v1_storage_backend_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestStorageBackendDataPathRequest.ProtoReflect.Descriptor instead. +func (*TestStorageBackendDataPathRequest) Descriptor() ([]byte, []int) { + return file_rookery_v1_storage_backend_proto_rawDescGZIP(), []int{14} +} + +func (x *TestStorageBackendDataPathRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type StorageBackendHealth struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Reachable bool `protobuf:"varint,2,opt,name=reachable,proto3" json:"reachable,omitempty"` + Ready bool `protobuf:"varint,3,opt,name=ready,proto3" json:"ready,omitempty"` + Driver string `protobuf:"bytes,4,opt,name=driver,proto3" json:"driver,omitempty"` + StorageClass string `protobuf:"bytes,5,opt,name=storage_class,json=storageClass,proto3" json:"storage_class,omitempty"` + Protocols []string `protobuf:"bytes,6,rep,name=protocols,proto3" json:"protocols,omitempty"` + QemuReady bool `protobuf:"varint,7,opt,name=qemu_ready,json=qemuReady,proto3" json:"qemu_ready,omitempty"` + Detail string `protobuf:"bytes,8,opt,name=detail,proto3" json:"detail,omitempty"` + CheckedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=checked_at,json=checkedAt,proto3" json:"checked_at,omitempty"` + Classes []*StorageClassCapacity `protobuf:"bytes,10,rep,name=classes,proto3" json:"classes,omitempty"` + PendingDeletions int64 `protobuf:"varint,11,opt,name=pending_deletions,json=pendingDeletions,proto3" json:"pending_deletions,omitempty"` + OldestPendingDeletionAgeSeconds int64 `protobuf:"varint,12,opt,name=oldest_pending_deletion_age_seconds,json=oldestPendingDeletionAgeSeconds,proto3" json:"oldest_pending_deletion_age_seconds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StorageBackendHealth) Reset() { + *x = StorageBackendHealth{} + mi := &file_rookery_v1_storage_backend_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StorageBackendHealth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StorageBackendHealth) ProtoMessage() {} + +func (x *StorageBackendHealth) ProtoReflect() protoreflect.Message { + mi := &file_rookery_v1_storage_backend_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StorageBackendHealth.ProtoReflect.Descriptor instead. +func (*StorageBackendHealth) Descriptor() ([]byte, []int) { + return file_rookery_v1_storage_backend_proto_rawDescGZIP(), []int{15} +} + +func (x *StorageBackendHealth) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *StorageBackendHealth) GetReachable() bool { + if x != nil { + return x.Reachable + } + return false +} + +func (x *StorageBackendHealth) GetReady() bool { + if x != nil { + return x.Ready + } + return false +} + +func (x *StorageBackendHealth) GetDriver() string { + if x != nil { + return x.Driver + } + return "" +} + +func (x *StorageBackendHealth) GetStorageClass() string { + if x != nil { + return x.StorageClass + } + return "" +} + +func (x *StorageBackendHealth) GetProtocols() []string { + if x != nil { + return x.Protocols + } + return nil +} + +func (x *StorageBackendHealth) GetQemuReady() bool { + if x != nil { + return x.QemuReady + } + return false +} + +func (x *StorageBackendHealth) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +func (x *StorageBackendHealth) GetCheckedAt() *timestamppb.Timestamp { + if x != nil { + return x.CheckedAt + } + return nil +} + +func (x *StorageBackendHealth) GetClasses() []*StorageClassCapacity { + if x != nil { + return x.Classes + } + return nil +} + +func (x *StorageBackendHealth) GetPendingDeletions() int64 { + if x != nil { + return x.PendingDeletions + } + return 0 +} + +func (x *StorageBackendHealth) GetOldestPendingDeletionAgeSeconds() int64 { + if x != nil { + return x.OldestPendingDeletionAgeSeconds + } + return 0 +} + +type StorageClassCapacity struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + VgName string `protobuf:"bytes,2,opt,name=vg_name,json=vgName,proto3" json:"vg_name,omitempty"` + State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` + TotalBytes int64 `protobuf:"varint,4,opt,name=total_bytes,json=totalBytes,proto3" json:"total_bytes,omitempty"` + FreeBytes int64 `protobuf:"varint,5,opt,name=free_bytes,json=freeBytes,proto3" json:"free_bytes,omitempty"` + AllocatableBytes int64 `protobuf:"varint,6,opt,name=allocatable_bytes,json=allocatableBytes,proto3" json:"allocatable_bytes,omitempty"` + MaxVolumeBytes int64 `protobuf:"varint,7,opt,name=max_volume_bytes,json=maxVolumeBytes,proto3" json:"max_volume_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StorageClassCapacity) Reset() { + *x = StorageClassCapacity{} + mi := &file_rookery_v1_storage_backend_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StorageClassCapacity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StorageClassCapacity) ProtoMessage() {} + +func (x *StorageClassCapacity) ProtoReflect() protoreflect.Message { + mi := &file_rookery_v1_storage_backend_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StorageClassCapacity.ProtoReflect.Descriptor instead. +func (*StorageClassCapacity) Descriptor() ([]byte, []int) { + return file_rookery_v1_storage_backend_proto_rawDescGZIP(), []int{16} +} + +func (x *StorageClassCapacity) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StorageClassCapacity) GetVgName() string { + if x != nil { + return x.VgName + } + return "" +} + +func (x *StorageClassCapacity) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *StorageClassCapacity) GetTotalBytes() int64 { + if x != nil { + return x.TotalBytes + } + return 0 +} + +func (x *StorageClassCapacity) GetFreeBytes() int64 { + if x != nil { + return x.FreeBytes + } + return 0 +} + +func (x *StorageClassCapacity) GetAllocatableBytes() int64 { + if x != nil { + return x.AllocatableBytes + } + return 0 +} + +func (x *StorageClassCapacity) GetMaxVolumeBytes() int64 { + if x != nil { + return x.MaxVolumeBytes + } + return 0 +} + var File_rookery_v1_storage_backend_proto protoreflect.FileDescriptor const file_rookery_v1_storage_backend_proto_rawDesc = "" + @@ -945,7 +1257,38 @@ const file_rookery_v1_storage_backend_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\x03B\a\xfaB\x04\"\x02 \x00R\x02id\x12*\n" + "\x05names\x18\x02 \x03(\tB\x14\xfaB\x11\x92\x01\x0e\b\x01\x10\x80\x02\"\ar\x05\x10\x01\x18\x80\x01R\x05names\"=\n" + "\x13ImportDisksResponse\x12&\n" + - "\x05disks\x18\x01 \x03(\v2\x10.rookery.v1.DiskR\x05disks2\xd6\a\n" + + "\x05disks\x18\x01 \x03(\v2\x10.rookery.v1.DiskR\x05disks\"9\n" + + "\x1eGetStorageBackendHealthRequest\x12\x17\n" + + "\x02id\x18\x01 \x01(\x03B\a\xfaB\x04\"\x02 \x00R\x02id\"<\n" + + "!TestStorageBackendDataPathRequest\x12\x17\n" + + "\x02id\x18\x01 \x01(\x03B\a\xfaB\x04\"\x02 \x00R\x02id\"\xde\x03\n" + + "\x14StorageBackendHealth\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1c\n" + + "\treachable\x18\x02 \x01(\bR\treachable\x12\x14\n" + + "\x05ready\x18\x03 \x01(\bR\x05ready\x12\x16\n" + + "\x06driver\x18\x04 \x01(\tR\x06driver\x12#\n" + + "\rstorage_class\x18\x05 \x01(\tR\fstorageClass\x12\x1c\n" + + "\tprotocols\x18\x06 \x03(\tR\tprotocols\x12\x1d\n" + + "\n" + + "qemu_ready\x18\a \x01(\bR\tqemuReady\x12\x16\n" + + "\x06detail\x18\b \x01(\tR\x06detail\x129\n" + + "\n" + + "checked_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcheckedAt\x12:\n" + + "\aclasses\x18\n" + + " \x03(\v2 .rookery.v1.StorageClassCapacityR\aclasses\x12+\n" + + "\x11pending_deletions\x18\v \x01(\x03R\x10pendingDeletions\x12L\n" + + "#oldest_pending_deletion_age_seconds\x18\f \x01(\x03R\x1foldestPendingDeletionAgeSeconds\"\xf0\x01\n" + + "\x14StorageClassCapacity\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x17\n" + + "\avg_name\x18\x02 \x01(\tR\x06vgName\x12\x14\n" + + "\x05state\x18\x03 \x01(\tR\x05state\x12\x1f\n" + + "\vtotal_bytes\x18\x04 \x01(\x03R\n" + + "totalBytes\x12\x1d\n" + + "\n" + + "free_bytes\x18\x05 \x01(\x03R\tfreeBytes\x12+\n" + + "\x11allocatable_bytes\x18\x06 \x01(\x03R\x10allocatableBytes\x12(\n" + + "\x10max_volume_bytes\x18\a \x01(\x03R\x0emaxVolumeBytes2\x87\n" + + "\n" + "\x15StorageBackendService\x12\x8f\x01\n" + "\x13ListStorageBackends\x12&.rookery.v1.ListStorageBackendsRequest\x1a'.rookery.v1.ListStorageBackendsResponse\"'\x82\xd3\xe4\x93\x02!b\bbackends\x12\x15/api/storage-backends\x12y\n" + "\x11GetStorageBackend\x12$.rookery.v1.GetStorageBackendRequest\x1a\x1a.rookery.v1.StorageBackend\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/api/storage-backends/{id}\x12}\n" + @@ -953,7 +1296,9 @@ const file_rookery_v1_storage_backend_proto_rawDesc = "" + "\x14UpdateStorageBackend\x12'.rookery.v1.UpdateStorageBackendRequest\x1a\x1a.rookery.v1.StorageBackend\"%\x82\xd3\xe4\x93\x02\x1f:\x01*\x1a\x1a/api/storage-backends/{id}\x12{\n" + "\x14DeleteStorageBackend\x12'.rookery.v1.DeleteStorageBackendRequest\x1a\x16.google.protobuf.Empty\"\"\x82\xd3\xe4\x93\x02\x1c*\x1a/api/storage-backends/{id}\x12\xa2\x01\n" + "\x13ListImportableDisks\x12&.rookery.v1.ListImportableDisksRequest\x1a'.rookery.v1.ListImportableDisksResponse\":\x82\xd3\xe4\x93\x024b\x05disks\x12+/api/storage-backends/{id}/importable-disks\x12\x89\x01\n" + - "\vImportDisks\x12\x1e.rookery.v1.ImportDisksRequest\x1a\x1f.rookery.v1.ImportDisksResponse\"9\x82\xd3\xe4\x93\x023:\x01*b\x05disks\"'/api/storage-backends/{id}/import-disksB?Z=github.com/voidstarit/rookery/design/gen/rookery/v1;rookeryv1b\x06proto3" + "\vImportDisks\x12\x1e.rookery.v1.ImportDisksRequest\x1a\x1f.rookery.v1.ImportDisksResponse\"9\x82\xd3\xe4\x93\x023:\x01*b\x05disks\"'/api/storage-backends/{id}/import-disks\x12\x92\x01\n" + + "\x17GetStorageBackendHealth\x12*.rookery.v1.GetStorageBackendHealthRequest\x1a .rookery.v1.StorageBackendHealth\")\x82\xd3\xe4\x93\x02#\x12!/api/storage-backends/{id}/health\x12\x99\x01\n" + + "\x1aTestStorageBackendDataPath\x12-.rookery.v1.TestStorageBackendDataPathRequest\x1a .rookery.v1.StorageBackendHealth\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/api/storage-backends/{id}/testB?Z=github.com/voidstarit/rookery/design/gen/rookery/v1;rookeryv1b\x06proto3" var ( file_rookery_v1_storage_backend_proto_rawDescOnce sync.Once @@ -967,55 +1312,65 @@ func file_rookery_v1_storage_backend_proto_rawDescGZIP() []byte { return file_rookery_v1_storage_backend_proto_rawDescData } -var file_rookery_v1_storage_backend_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_rookery_v1_storage_backend_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_rookery_v1_storage_backend_proto_goTypes = []any{ - (*StorageBackendConfig)(nil), // 0: rookery.v1.StorageBackendConfig - (*KoujaConfig)(nil), // 1: rookery.v1.KoujaConfig - (*StorageBackend)(nil), // 2: rookery.v1.StorageBackend - (*ListStorageBackendsRequest)(nil), // 3: rookery.v1.ListStorageBackendsRequest - (*ListStorageBackendsResponse)(nil), // 4: rookery.v1.ListStorageBackendsResponse - (*GetStorageBackendRequest)(nil), // 5: rookery.v1.GetStorageBackendRequest - (*CreateStorageBackendRequest)(nil), // 6: rookery.v1.CreateStorageBackendRequest - (*UpdateStorageBackendRequest)(nil), // 7: rookery.v1.UpdateStorageBackendRequest - (*DeleteStorageBackendRequest)(nil), // 8: rookery.v1.DeleteStorageBackendRequest - (*ListImportableDisksRequest)(nil), // 9: rookery.v1.ListImportableDisksRequest - (*ListImportableDisksResponse)(nil), // 10: rookery.v1.ListImportableDisksResponse - (*ImportDisksRequest)(nil), // 11: rookery.v1.ImportDisksRequest - (*ImportDisksResponse)(nil), // 12: rookery.v1.ImportDisksResponse - (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp - (*ImportableDisk)(nil), // 14: rookery.v1.ImportableDisk - (*Disk)(nil), // 15: rookery.v1.Disk - (*emptypb.Empty)(nil), // 16: google.protobuf.Empty + (*StorageBackendConfig)(nil), // 0: rookery.v1.StorageBackendConfig + (*KoujaConfig)(nil), // 1: rookery.v1.KoujaConfig + (*StorageBackend)(nil), // 2: rookery.v1.StorageBackend + (*ListStorageBackendsRequest)(nil), // 3: rookery.v1.ListStorageBackendsRequest + (*ListStorageBackendsResponse)(nil), // 4: rookery.v1.ListStorageBackendsResponse + (*GetStorageBackendRequest)(nil), // 5: rookery.v1.GetStorageBackendRequest + (*CreateStorageBackendRequest)(nil), // 6: rookery.v1.CreateStorageBackendRequest + (*UpdateStorageBackendRequest)(nil), // 7: rookery.v1.UpdateStorageBackendRequest + (*DeleteStorageBackendRequest)(nil), // 8: rookery.v1.DeleteStorageBackendRequest + (*ListImportableDisksRequest)(nil), // 9: rookery.v1.ListImportableDisksRequest + (*ListImportableDisksResponse)(nil), // 10: rookery.v1.ListImportableDisksResponse + (*ImportDisksRequest)(nil), // 11: rookery.v1.ImportDisksRequest + (*ImportDisksResponse)(nil), // 12: rookery.v1.ImportDisksResponse + (*GetStorageBackendHealthRequest)(nil), // 13: rookery.v1.GetStorageBackendHealthRequest + (*TestStorageBackendDataPathRequest)(nil), // 14: rookery.v1.TestStorageBackendDataPathRequest + (*StorageBackendHealth)(nil), // 15: rookery.v1.StorageBackendHealth + (*StorageClassCapacity)(nil), // 16: rookery.v1.StorageClassCapacity + (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp + (*ImportableDisk)(nil), // 18: rookery.v1.ImportableDisk + (*Disk)(nil), // 19: rookery.v1.Disk + (*emptypb.Empty)(nil), // 20: google.protobuf.Empty } var file_rookery_v1_storage_backend_proto_depIdxs = []int32{ 1, // 0: rookery.v1.StorageBackendConfig.kouja:type_name -> rookery.v1.KoujaConfig 0, // 1: rookery.v1.StorageBackend.config:type_name -> rookery.v1.StorageBackendConfig - 13, // 2: rookery.v1.StorageBackend.created_at:type_name -> google.protobuf.Timestamp - 13, // 3: rookery.v1.StorageBackend.updated_at:type_name -> google.protobuf.Timestamp + 17, // 2: rookery.v1.StorageBackend.created_at:type_name -> google.protobuf.Timestamp + 17, // 3: rookery.v1.StorageBackend.updated_at:type_name -> google.protobuf.Timestamp 2, // 4: rookery.v1.ListStorageBackendsResponse.backends:type_name -> rookery.v1.StorageBackend 0, // 5: rookery.v1.CreateStorageBackendRequest.config:type_name -> rookery.v1.StorageBackendConfig 0, // 6: rookery.v1.UpdateStorageBackendRequest.config:type_name -> rookery.v1.StorageBackendConfig - 14, // 7: rookery.v1.ListImportableDisksResponse.disks:type_name -> rookery.v1.ImportableDisk - 15, // 8: rookery.v1.ImportDisksResponse.disks:type_name -> rookery.v1.Disk - 3, // 9: rookery.v1.StorageBackendService.ListStorageBackends:input_type -> rookery.v1.ListStorageBackendsRequest - 5, // 10: rookery.v1.StorageBackendService.GetStorageBackend:input_type -> rookery.v1.GetStorageBackendRequest - 6, // 11: rookery.v1.StorageBackendService.CreateStorageBackend:input_type -> rookery.v1.CreateStorageBackendRequest - 7, // 12: rookery.v1.StorageBackendService.UpdateStorageBackend:input_type -> rookery.v1.UpdateStorageBackendRequest - 8, // 13: rookery.v1.StorageBackendService.DeleteStorageBackend:input_type -> rookery.v1.DeleteStorageBackendRequest - 9, // 14: rookery.v1.StorageBackendService.ListImportableDisks:input_type -> rookery.v1.ListImportableDisksRequest - 11, // 15: rookery.v1.StorageBackendService.ImportDisks:input_type -> rookery.v1.ImportDisksRequest - 4, // 16: rookery.v1.StorageBackendService.ListStorageBackends:output_type -> rookery.v1.ListStorageBackendsResponse - 2, // 17: rookery.v1.StorageBackendService.GetStorageBackend:output_type -> rookery.v1.StorageBackend - 2, // 18: rookery.v1.StorageBackendService.CreateStorageBackend:output_type -> rookery.v1.StorageBackend - 2, // 19: rookery.v1.StorageBackendService.UpdateStorageBackend:output_type -> rookery.v1.StorageBackend - 16, // 20: rookery.v1.StorageBackendService.DeleteStorageBackend:output_type -> google.protobuf.Empty - 10, // 21: rookery.v1.StorageBackendService.ListImportableDisks:output_type -> rookery.v1.ListImportableDisksResponse - 12, // 22: rookery.v1.StorageBackendService.ImportDisks:output_type -> rookery.v1.ImportDisksResponse - 16, // [16:23] is the sub-list for method output_type - 9, // [9:16] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 18, // 7: rookery.v1.ListImportableDisksResponse.disks:type_name -> rookery.v1.ImportableDisk + 19, // 8: rookery.v1.ImportDisksResponse.disks:type_name -> rookery.v1.Disk + 17, // 9: rookery.v1.StorageBackendHealth.checked_at:type_name -> google.protobuf.Timestamp + 16, // 10: rookery.v1.StorageBackendHealth.classes:type_name -> rookery.v1.StorageClassCapacity + 3, // 11: rookery.v1.StorageBackendService.ListStorageBackends:input_type -> rookery.v1.ListStorageBackendsRequest + 5, // 12: rookery.v1.StorageBackendService.GetStorageBackend:input_type -> rookery.v1.GetStorageBackendRequest + 6, // 13: rookery.v1.StorageBackendService.CreateStorageBackend:input_type -> rookery.v1.CreateStorageBackendRequest + 7, // 14: rookery.v1.StorageBackendService.UpdateStorageBackend:input_type -> rookery.v1.UpdateStorageBackendRequest + 8, // 15: rookery.v1.StorageBackendService.DeleteStorageBackend:input_type -> rookery.v1.DeleteStorageBackendRequest + 9, // 16: rookery.v1.StorageBackendService.ListImportableDisks:input_type -> rookery.v1.ListImportableDisksRequest + 11, // 17: rookery.v1.StorageBackendService.ImportDisks:input_type -> rookery.v1.ImportDisksRequest + 13, // 18: rookery.v1.StorageBackendService.GetStorageBackendHealth:input_type -> rookery.v1.GetStorageBackendHealthRequest + 14, // 19: rookery.v1.StorageBackendService.TestStorageBackendDataPath:input_type -> rookery.v1.TestStorageBackendDataPathRequest + 4, // 20: rookery.v1.StorageBackendService.ListStorageBackends:output_type -> rookery.v1.ListStorageBackendsResponse + 2, // 21: rookery.v1.StorageBackendService.GetStorageBackend:output_type -> rookery.v1.StorageBackend + 2, // 22: rookery.v1.StorageBackendService.CreateStorageBackend:output_type -> rookery.v1.StorageBackend + 2, // 23: rookery.v1.StorageBackendService.UpdateStorageBackend:output_type -> rookery.v1.StorageBackend + 20, // 24: rookery.v1.StorageBackendService.DeleteStorageBackend:output_type -> google.protobuf.Empty + 10, // 25: rookery.v1.StorageBackendService.ListImportableDisks:output_type -> rookery.v1.ListImportableDisksResponse + 12, // 26: rookery.v1.StorageBackendService.ImportDisks:output_type -> rookery.v1.ImportDisksResponse + 15, // 27: rookery.v1.StorageBackendService.GetStorageBackendHealth:output_type -> rookery.v1.StorageBackendHealth + 15, // 28: rookery.v1.StorageBackendService.TestStorageBackendDataPath:output_type -> rookery.v1.StorageBackendHealth + 20, // [20:29] is the sub-list for method output_type + 11, // [11:20] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_rookery_v1_storage_backend_proto_init() } @@ -1033,7 +1388,7 @@ func file_rookery_v1_storage_backend_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_rookery_v1_storage_backend_proto_rawDesc), len(file_rookery_v1_storage_backend_proto_rawDesc)), NumEnums: 0, - NumMessages: 13, + NumMessages: 17, NumExtensions: 0, NumServices: 1, }, diff --git a/design/gen/rookery/v1/storage_backend.pb.gw.go b/design/gen/rookery/v1/storage_backend.pb.gw.go index 84cd657..ca9d81a 100644 --- a/design/gen/rookery/v1/storage_backend.pb.gw.go +++ b/design/gen/rookery/v1/storage_backend.pb.gw.go @@ -290,6 +290,90 @@ func local_request_StorageBackendService_ImportDisks_0(ctx context.Context, mars return msg, metadata, err } +func request_StorageBackendService_GetStorageBackendHealth_0(ctx context.Context, marshaler runtime.Marshaler, client StorageBackendServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetStorageBackendHealthRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.Int64(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.GetStorageBackendHealth(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_StorageBackendService_GetStorageBackendHealth_0(ctx context.Context, marshaler runtime.Marshaler, server StorageBackendServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetStorageBackendHealthRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.Int64(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.GetStorageBackendHealth(ctx, &protoReq) + return msg, metadata, err +} + +func request_StorageBackendService_TestStorageBackendDataPath_0(ctx context.Context, marshaler runtime.Marshaler, client StorageBackendServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq TestStorageBackendDataPathRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.Int64(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.TestStorageBackendDataPath(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_StorageBackendService_TestStorageBackendDataPath_0(ctx context.Context, marshaler runtime.Marshaler, server StorageBackendServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq TestStorageBackendDataPathRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.Int64(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.TestStorageBackendDataPath(ctx, &protoReq) + return msg, metadata, err +} + // RegisterStorageBackendServiceHandlerServer registers the http handlers for service StorageBackendService to "mux". // UnaryRPC :call StorageBackendServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -436,6 +520,46 @@ func RegisterStorageBackendServiceHandlerServer(ctx context.Context, mux *runtim } forward_StorageBackendService_ImportDisks_0(annotatedContext, mux, outboundMarshaler, w, req, response_StorageBackendService_ImportDisks_0{resp.(*ImportDisksResponse)}, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_StorageBackendService_GetStorageBackendHealth_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rookery.v1.StorageBackendService/GetStorageBackendHealth", runtime.WithHTTPPathPattern("/api/storage-backends/{id}/health")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_StorageBackendService_GetStorageBackendHealth_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_StorageBackendService_GetStorageBackendHealth_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_StorageBackendService_TestStorageBackendDataPath_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rookery.v1.StorageBackendService/TestStorageBackendDataPath", runtime.WithHTTPPathPattern("/api/storage-backends/{id}/test")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_StorageBackendService_TestStorageBackendDataPath_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_StorageBackendService_TestStorageBackendDataPath_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -595,6 +719,40 @@ func RegisterStorageBackendServiceHandlerClient(ctx context.Context, mux *runtim } forward_StorageBackendService_ImportDisks_0(annotatedContext, mux, outboundMarshaler, w, req, response_StorageBackendService_ImportDisks_0{resp.(*ImportDisksResponse)}, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_StorageBackendService_GetStorageBackendHealth_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/rookery.v1.StorageBackendService/GetStorageBackendHealth", runtime.WithHTTPPathPattern("/api/storage-backends/{id}/health")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_StorageBackendService_GetStorageBackendHealth_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_StorageBackendService_GetStorageBackendHealth_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_StorageBackendService_TestStorageBackendDataPath_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/rookery.v1.StorageBackendService/TestStorageBackendDataPath", runtime.WithHTTPPathPattern("/api/storage-backends/{id}/test")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_StorageBackendService_TestStorageBackendDataPath_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_StorageBackendService_TestStorageBackendDataPath_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -626,21 +784,25 @@ func (m response_StorageBackendService_ImportDisks_0) XXX_ResponseBody() interfa } var ( - pattern_StorageBackendService_ListStorageBackends_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "storage-backends"}, "")) - pattern_StorageBackendService_GetStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "storage-backends", "id"}, "")) - pattern_StorageBackendService_CreateStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "storage-backends"}, "")) - pattern_StorageBackendService_UpdateStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "storage-backends", "id"}, "")) - pattern_StorageBackendService_DeleteStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "storage-backends", "id"}, "")) - pattern_StorageBackendService_ListImportableDisks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "storage-backends", "id", "importable-disks"}, "")) - pattern_StorageBackendService_ImportDisks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "storage-backends", "id", "import-disks"}, "")) + pattern_StorageBackendService_ListStorageBackends_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "storage-backends"}, "")) + pattern_StorageBackendService_GetStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "storage-backends", "id"}, "")) + pattern_StorageBackendService_CreateStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "storage-backends"}, "")) + pattern_StorageBackendService_UpdateStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "storage-backends", "id"}, "")) + pattern_StorageBackendService_DeleteStorageBackend_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "storage-backends", "id"}, "")) + pattern_StorageBackendService_ListImportableDisks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "storage-backends", "id", "importable-disks"}, "")) + pattern_StorageBackendService_ImportDisks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "storage-backends", "id", "import-disks"}, "")) + pattern_StorageBackendService_GetStorageBackendHealth_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "storage-backends", "id", "health"}, "")) + pattern_StorageBackendService_TestStorageBackendDataPath_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "storage-backends", "id", "test"}, "")) ) var ( - forward_StorageBackendService_ListStorageBackends_0 = runtime.ForwardResponseMessage - forward_StorageBackendService_GetStorageBackend_0 = runtime.ForwardResponseMessage - forward_StorageBackendService_CreateStorageBackend_0 = runtime.ForwardResponseMessage - forward_StorageBackendService_UpdateStorageBackend_0 = runtime.ForwardResponseMessage - forward_StorageBackendService_DeleteStorageBackend_0 = runtime.ForwardResponseMessage - forward_StorageBackendService_ListImportableDisks_0 = runtime.ForwardResponseMessage - forward_StorageBackendService_ImportDisks_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_ListStorageBackends_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_GetStorageBackend_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_CreateStorageBackend_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_UpdateStorageBackend_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_DeleteStorageBackend_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_ListImportableDisks_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_ImportDisks_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_GetStorageBackendHealth_0 = runtime.ForwardResponseMessage + forward_StorageBackendService_TestStorageBackendDataPath_0 = runtime.ForwardResponseMessage ) diff --git a/design/gen/rookery/v1/storage_backend.pb.validate.go b/design/gen/rookery/v1/storage_backend.pb.validate.go index 29a5980..44aa715 100644 --- a/design/gen/rookery/v1/storage_backend.pb.validate.go +++ b/design/gen/rookery/v1/storage_backend.pb.validate.go @@ -2046,3 +2046,532 @@ var _ interface { Cause() error ErrorName() string } = ImportDisksResponseValidationError{} + +// Validate checks the field values on GetStorageBackendHealthRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *GetStorageBackendHealthRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on GetStorageBackendHealthRequest with +// the rules defined in the proto definition for this message. If any rules +// are violated, the result is a list of violation errors wrapped in +// GetStorageBackendHealthRequestMultiError, or nil if none found. +func (m *GetStorageBackendHealthRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *GetStorageBackendHealthRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if m.GetId() <= 0 { + err := GetStorageBackendHealthRequestValidationError{ + field: "Id", + reason: "value must be greater than 0", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return GetStorageBackendHealthRequestMultiError(errors) + } + + return nil +} + +// GetStorageBackendHealthRequestMultiError is an error wrapping multiple +// validation errors returned by GetStorageBackendHealthRequest.ValidateAll() +// if the designated constraints aren't met. +type GetStorageBackendHealthRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m GetStorageBackendHealthRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m GetStorageBackendHealthRequestMultiError) AllErrors() []error { return m } + +// GetStorageBackendHealthRequestValidationError is the validation error +// returned by GetStorageBackendHealthRequest.Validate if the designated +// constraints aren't met. +type GetStorageBackendHealthRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e GetStorageBackendHealthRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e GetStorageBackendHealthRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e GetStorageBackendHealthRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e GetStorageBackendHealthRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e GetStorageBackendHealthRequestValidationError) ErrorName() string { + return "GetStorageBackendHealthRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e GetStorageBackendHealthRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sGetStorageBackendHealthRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = GetStorageBackendHealthRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = GetStorageBackendHealthRequestValidationError{} + +// Validate checks the field values on TestStorageBackendDataPathRequest with +// the rules defined in the proto definition for this message. If any rules +// are violated, the first error encountered is returned, or nil if there are +// no violations. +func (m *TestStorageBackendDataPathRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TestStorageBackendDataPathRequest +// with the rules defined in the proto definition for this message. If any +// rules are violated, the result is a list of violation errors wrapped in +// TestStorageBackendDataPathRequestMultiError, or nil if none found. +func (m *TestStorageBackendDataPathRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *TestStorageBackendDataPathRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if m.GetId() <= 0 { + err := TestStorageBackendDataPathRequestValidationError{ + field: "Id", + reason: "value must be greater than 0", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return TestStorageBackendDataPathRequestMultiError(errors) + } + + return nil +} + +// TestStorageBackendDataPathRequestMultiError is an error wrapping multiple +// validation errors returned by +// TestStorageBackendDataPathRequest.ValidateAll() if the designated +// constraints aren't met. +type TestStorageBackendDataPathRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TestStorageBackendDataPathRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TestStorageBackendDataPathRequestMultiError) AllErrors() []error { return m } + +// TestStorageBackendDataPathRequestValidationError is the validation error +// returned by TestStorageBackendDataPathRequest.Validate if the designated +// constraints aren't met. +type TestStorageBackendDataPathRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TestStorageBackendDataPathRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TestStorageBackendDataPathRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TestStorageBackendDataPathRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TestStorageBackendDataPathRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TestStorageBackendDataPathRequestValidationError) ErrorName() string { + return "TestStorageBackendDataPathRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e TestStorageBackendDataPathRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTestStorageBackendDataPathRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TestStorageBackendDataPathRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TestStorageBackendDataPathRequestValidationError{} + +// Validate checks the field values on StorageBackendHealth with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *StorageBackendHealth) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on StorageBackendHealth with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// StorageBackendHealthMultiError, or nil if none found. +func (m *StorageBackendHealth) ValidateAll() error { + return m.validate(true) +} + +func (m *StorageBackendHealth) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Reachable + + // no validation rules for Ready + + // no validation rules for Driver + + // no validation rules for StorageClass + + // no validation rules for QemuReady + + // no validation rules for Detail + + if all { + switch v := interface{}(m.GetCheckedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, StorageBackendHealthValidationError{ + field: "CheckedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, StorageBackendHealthValidationError{ + field: "CheckedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCheckedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return StorageBackendHealthValidationError{ + field: "CheckedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + for idx, item := range m.GetClasses() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, StorageBackendHealthValidationError{ + field: fmt.Sprintf("Classes[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, StorageBackendHealthValidationError{ + field: fmt.Sprintf("Classes[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return StorageBackendHealthValidationError{ + field: fmt.Sprintf("Classes[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + // no validation rules for PendingDeletions + + // no validation rules for OldestPendingDeletionAgeSeconds + + if len(errors) > 0 { + return StorageBackendHealthMultiError(errors) + } + + return nil +} + +// StorageBackendHealthMultiError is an error wrapping multiple validation +// errors returned by StorageBackendHealth.ValidateAll() if the designated +// constraints aren't met. +type StorageBackendHealthMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m StorageBackendHealthMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m StorageBackendHealthMultiError) AllErrors() []error { return m } + +// StorageBackendHealthValidationError is the validation error returned by +// StorageBackendHealth.Validate if the designated constraints aren't met. +type StorageBackendHealthValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e StorageBackendHealthValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e StorageBackendHealthValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e StorageBackendHealthValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e StorageBackendHealthValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e StorageBackendHealthValidationError) ErrorName() string { + return "StorageBackendHealthValidationError" +} + +// Error satisfies the builtin error interface +func (e StorageBackendHealthValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sStorageBackendHealth.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = StorageBackendHealthValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = StorageBackendHealthValidationError{} + +// Validate checks the field values on StorageClassCapacity with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *StorageClassCapacity) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on StorageClassCapacity with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// StorageClassCapacityMultiError, or nil if none found. +func (m *StorageClassCapacity) ValidateAll() error { + return m.validate(true) +} + +func (m *StorageClassCapacity) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + // no validation rules for VgName + + // no validation rules for State + + // no validation rules for TotalBytes + + // no validation rules for FreeBytes + + // no validation rules for AllocatableBytes + + // no validation rules for MaxVolumeBytes + + if len(errors) > 0 { + return StorageClassCapacityMultiError(errors) + } + + return nil +} + +// StorageClassCapacityMultiError is an error wrapping multiple validation +// errors returned by StorageClassCapacity.ValidateAll() if the designated +// constraints aren't met. +type StorageClassCapacityMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m StorageClassCapacityMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m StorageClassCapacityMultiError) AllErrors() []error { return m } + +// StorageClassCapacityValidationError is the validation error returned by +// StorageClassCapacity.Validate if the designated constraints aren't met. +type StorageClassCapacityValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e StorageClassCapacityValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e StorageClassCapacityValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e StorageClassCapacityValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e StorageClassCapacityValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e StorageClassCapacityValidationError) ErrorName() string { + return "StorageClassCapacityValidationError" +} + +// Error satisfies the builtin error interface +func (e StorageClassCapacityValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sStorageClassCapacity.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = StorageClassCapacityValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = StorageClassCapacityValidationError{} diff --git a/design/gen/rookery/v1/storage_backend_grpc.pb.go b/design/gen/rookery/v1/storage_backend_grpc.pb.go index 402dcdf..db43258 100644 --- a/design/gen/rookery/v1/storage_backend_grpc.pb.go +++ b/design/gen/rookery/v1/storage_backend_grpc.pb.go @@ -20,13 +20,15 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StorageBackendService_ListStorageBackends_FullMethodName = "/rookery.v1.StorageBackendService/ListStorageBackends" - StorageBackendService_GetStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/GetStorageBackend" - StorageBackendService_CreateStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/CreateStorageBackend" - StorageBackendService_UpdateStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/UpdateStorageBackend" - StorageBackendService_DeleteStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/DeleteStorageBackend" - StorageBackendService_ListImportableDisks_FullMethodName = "/rookery.v1.StorageBackendService/ListImportableDisks" - StorageBackendService_ImportDisks_FullMethodName = "/rookery.v1.StorageBackendService/ImportDisks" + StorageBackendService_ListStorageBackends_FullMethodName = "/rookery.v1.StorageBackendService/ListStorageBackends" + StorageBackendService_GetStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/GetStorageBackend" + StorageBackendService_CreateStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/CreateStorageBackend" + StorageBackendService_UpdateStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/UpdateStorageBackend" + StorageBackendService_DeleteStorageBackend_FullMethodName = "/rookery.v1.StorageBackendService/DeleteStorageBackend" + StorageBackendService_ListImportableDisks_FullMethodName = "/rookery.v1.StorageBackendService/ListImportableDisks" + StorageBackendService_ImportDisks_FullMethodName = "/rookery.v1.StorageBackendService/ImportDisks" + StorageBackendService_GetStorageBackendHealth_FullMethodName = "/rookery.v1.StorageBackendService/GetStorageBackendHealth" + StorageBackendService_TestStorageBackendDataPath_FullMethodName = "/rookery.v1.StorageBackendService/TestStorageBackendDataPath" ) // StorageBackendServiceClient is the client API for StorageBackendService service. @@ -42,6 +44,8 @@ type StorageBackendServiceClient interface { DeleteStorageBackend(ctx context.Context, in *DeleteStorageBackendRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ListImportableDisks(ctx context.Context, in *ListImportableDisksRequest, opts ...grpc.CallOption) (*ListImportableDisksResponse, error) ImportDisks(ctx context.Context, in *ImportDisksRequest, opts ...grpc.CallOption) (*ImportDisksResponse, error) + GetStorageBackendHealth(ctx context.Context, in *GetStorageBackendHealthRequest, opts ...grpc.CallOption) (*StorageBackendHealth, error) + TestStorageBackendDataPath(ctx context.Context, in *TestStorageBackendDataPathRequest, opts ...grpc.CallOption) (*StorageBackendHealth, error) } type storageBackendServiceClient struct { @@ -122,6 +126,26 @@ func (c *storageBackendServiceClient) ImportDisks(ctx context.Context, in *Impor return out, nil } +func (c *storageBackendServiceClient) GetStorageBackendHealth(ctx context.Context, in *GetStorageBackendHealthRequest, opts ...grpc.CallOption) (*StorageBackendHealth, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StorageBackendHealth) + err := c.cc.Invoke(ctx, StorageBackendService_GetStorageBackendHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageBackendServiceClient) TestStorageBackendDataPath(ctx context.Context, in *TestStorageBackendDataPathRequest, opts ...grpc.CallOption) (*StorageBackendHealth, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StorageBackendHealth) + err := c.cc.Invoke(ctx, StorageBackendService_TestStorageBackendDataPath_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // StorageBackendServiceServer is the server API for StorageBackendService service. // All implementations must embed UnimplementedStorageBackendServiceServer // for forward compatibility. @@ -135,6 +159,8 @@ type StorageBackendServiceServer interface { DeleteStorageBackend(context.Context, *DeleteStorageBackendRequest) (*emptypb.Empty, error) ListImportableDisks(context.Context, *ListImportableDisksRequest) (*ListImportableDisksResponse, error) ImportDisks(context.Context, *ImportDisksRequest) (*ImportDisksResponse, error) + GetStorageBackendHealth(context.Context, *GetStorageBackendHealthRequest) (*StorageBackendHealth, error) + TestStorageBackendDataPath(context.Context, *TestStorageBackendDataPathRequest) (*StorageBackendHealth, error) mustEmbedUnimplementedStorageBackendServiceServer() } @@ -166,6 +192,12 @@ func (UnimplementedStorageBackendServiceServer) ListImportableDisks(context.Cont func (UnimplementedStorageBackendServiceServer) ImportDisks(context.Context, *ImportDisksRequest) (*ImportDisksResponse, error) { return nil, status.Error(codes.Unimplemented, "method ImportDisks not implemented") } +func (UnimplementedStorageBackendServiceServer) GetStorageBackendHealth(context.Context, *GetStorageBackendHealthRequest) (*StorageBackendHealth, error) { + return nil, status.Error(codes.Unimplemented, "method GetStorageBackendHealth not implemented") +} +func (UnimplementedStorageBackendServiceServer) TestStorageBackendDataPath(context.Context, *TestStorageBackendDataPathRequest) (*StorageBackendHealth, error) { + return nil, status.Error(codes.Unimplemented, "method TestStorageBackendDataPath not implemented") +} func (UnimplementedStorageBackendServiceServer) mustEmbedUnimplementedStorageBackendServiceServer() {} func (UnimplementedStorageBackendServiceServer) testEmbeddedByValue() {} @@ -313,6 +345,42 @@ func _StorageBackendService_ImportDisks_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _StorageBackendService_GetStorageBackendHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetStorageBackendHealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageBackendServiceServer).GetStorageBackendHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageBackendService_GetStorageBackendHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageBackendServiceServer).GetStorageBackendHealth(ctx, req.(*GetStorageBackendHealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageBackendService_TestStorageBackendDataPath_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestStorageBackendDataPathRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageBackendServiceServer).TestStorageBackendDataPath(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageBackendService_TestStorageBackendDataPath_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageBackendServiceServer).TestStorageBackendDataPath(ctx, req.(*TestStorageBackendDataPathRequest)) + } + return interceptor(ctx, in, info, handler) +} + // StorageBackendService_ServiceDesc is the grpc.ServiceDesc for StorageBackendService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -348,6 +416,14 @@ var StorageBackendService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ImportDisks", Handler: _StorageBackendService_ImportDisks_Handler, }, + { + MethodName: "GetStorageBackendHealth", + Handler: _StorageBackendService_GetStorageBackendHealth_Handler, + }, + { + MethodName: "TestStorageBackendDataPath", + Handler: _StorageBackendService_TestStorageBackendDataPath_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "rookery/v1/storage_backend.proto", diff --git a/design/openapi/rookery.swagger.json b/design/openapi/rookery.swagger.json index fcc263b..061783a 100644 --- a/design/openapi/rookery.swagger.json +++ b/design/openapi/rookery.swagger.json @@ -394,8 +394,7 @@ "200": { "description": "A successful response.", "schema": { - "type": "object", - "properties": {} + "$ref": "#/definitions/v1DeleteDiskResponse" } }, "default": { @@ -2504,6 +2503,37 @@ ] } }, + "/api/storage-backends/{id}/health": { + "get": { + "operationId": "StorageBackendService_GetStorageBackendHealth", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1StorageBackendHealth" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "format": "int64" + } + ], + "tags": [ + "StorageBackendService" + ] + } + }, "/api/storage-backends/{id}/import-disks": { "post": { "operationId": "StorageBackendService_ImportDisks", @@ -2582,6 +2612,45 @@ ] } }, + "/api/storage-backends/{id}/test": { + "post": { + "operationId": "StorageBackendService_TestStorageBackendDataPath", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1StorageBackendHealth" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "format": "int64" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/StorageBackendServiceTestStorageBackendDataPathBody" + } + } + ], + "tags": [ + "StorageBackendService" + ] + } + }, "/api/troubleshoot/acl-counters": { "get": { "operationId": "TroubleshootService_ListACLInterfaceCounters", @@ -4115,6 +4184,9 @@ } } }, + "StorageBackendServiceTestStorageBackendDataPathBody": { + "type": "object" + }, "StorageBackendServiceUpdateStorageBackendBody": { "type": "object", "properties": { @@ -5003,6 +5075,17 @@ }, "description": "DNSRecord is one local record served by a DNS VNF. Name is a fully qualified\ndomain name; wildcard names such as *.lab1.test are supported. Type is A,\nAAAA, CNAME, TXT, or PTR." }, + "v1DeleteDiskResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean" + }, + "state": { + "type": "string" + } + } + }, "v1Disk": { "type": "object", "properties": { @@ -6558,6 +6641,88 @@ } } }, + "v1StorageBackendHealth": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "int64" + }, + "reachable": { + "type": "boolean" + }, + "ready": { + "type": "boolean" + }, + "driver": { + "type": "string" + }, + "storage_class": { + "type": "string" + }, + "protocols": { + "type": "array", + "items": { + "type": "string" + } + }, + "qemu_ready": { + "type": "boolean" + }, + "detail": { + "type": "string" + }, + "checked_at": { + "type": "string", + "format": "date-time" + }, + "classes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1StorageClassCapacity" + } + }, + "pending_deletions": { + "type": "string", + "format": "int64" + }, + "oldest_pending_deletion_age_seconds": { + "type": "string", + "format": "int64" + } + } + }, + "v1StorageClassCapacity": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "vg_name": { + "type": "string" + }, + "state": { + "type": "string" + }, + "total_bytes": { + "type": "string", + "format": "int64" + }, + "free_bytes": { + "type": "string", + "format": "int64" + }, + "allocatable_bytes": { + "type": "string", + "format": "int64" + }, + "max_volume_bytes": { + "type": "string", + "format": "int64" + } + } + }, "v1ThinPoolStatus": { "type": "object", "properties": { diff --git a/design/proto/rookery/v1/disk.proto b/design/proto/rookery/v1/disk.proto index e0bc7d1..289dd55 100644 --- a/design/proto/rookery/v1/disk.proto +++ b/design/proto/rookery/v1/disk.proto @@ -3,7 +3,6 @@ syntax = "proto3"; package rookery.v1; import "google/api/annotations.proto"; -import "google/protobuf/empty.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -37,7 +36,7 @@ service DiskService { }; } - rpc DeleteDisk(DeleteDiskRequest) returns (google.protobuf.Empty) { + rpc DeleteDisk(DeleteDiskRequest) returns (DeleteDiskResponse) { option (google.api.http) = {delete: "/api/disks/{id}"}; } } @@ -133,6 +132,11 @@ message CreateDiskRequest { string host_device = 9 [(validate.rules).string = {ignore_empty: true, max_len: 4096}]; } +message DeleteDiskResponse { + bool accepted = 1; + string state = 2; +} + message UpdateDiskRequest { int64 id = 1 [(validate.rules).int64 = {gt: 0}]; optional bool is_template = 2; diff --git a/design/proto/rookery/v1/storage_backend.proto b/design/proto/rookery/v1/storage_backend.proto index e7aa7f3..5543123 100644 --- a/design/proto/rookery/v1/storage_backend.proto +++ b/design/proto/rookery/v1/storage_backend.proto @@ -55,6 +55,17 @@ service StorageBackendService { response_body: "disks" }; } + + rpc GetStorageBackendHealth(GetStorageBackendHealthRequest) returns (StorageBackendHealth) { + option (google.api.http) = {get: "/api/storage-backends/{id}/health"}; + } + + rpc TestStorageBackendDataPath(TestStorageBackendDataPathRequest) returns (StorageBackendHealth) { + option (google.api.http) = { + post: "/api/storage-backends/{id}/test" + body: "*" + }; + } } message StorageBackendConfig { @@ -151,3 +162,36 @@ message ImportDisksRequest { message ImportDisksResponse { repeated Disk disks = 1; } + +message GetStorageBackendHealthRequest { + int64 id = 1 [(validate.rules).int64 = {gt: 0}]; +} + +message TestStorageBackendDataPathRequest { + int64 id = 1 [(validate.rules).int64 = {gt: 0}]; +} + +message StorageBackendHealth { + int64 id = 1; + bool reachable = 2; + bool ready = 3; + string driver = 4; + string storage_class = 5; + repeated string protocols = 6; + bool qemu_ready = 7; + string detail = 8; + google.protobuf.Timestamp checked_at = 9; + repeated StorageClassCapacity classes = 10; + int64 pending_deletions = 11; + int64 oldest_pending_deletion_age_seconds = 12; +} + +message StorageClassCapacity { + string name = 1; + string vg_name = 2; + string state = 3; + int64 total_bytes = 4; + int64 free_bytes = 5; + int64 allocatable_bytes = 6; + int64 max_volume_bytes = 7; +} diff --git a/internal/api/api_test.go b/internal/api/api_test.go index e7b80bd..7606e7f 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -1571,8 +1571,8 @@ func TestVMCRUD(t *testing.T) { if recorder.Code != http.StatusNoContent { t.Fatalf("delete status = %d, body = %s", recorder.Code, recorder.Body.String()) } - if recorder.Body.Len() != 0 { - t.Fatalf("delete body = %q, want empty", recorder.Body.String()) + if body := strings.TrimSpace(recorder.Body.String()); body != "" && body != "{}" { + t.Fatalf("delete body = %q, want empty or {}", body) } recorder = do(t, handler, http.MethodGet, "/api/vms/"+created.ID, nil) @@ -1893,8 +1893,8 @@ func TestDiskCRUD(t *testing.T) { if recorder.Code != http.StatusNoContent { t.Fatalf("delete status = %d, body = %s", recorder.Code, recorder.Body.String()) } - if recorder.Body.Len() != 0 { - t.Fatalf("delete body = %q, want empty", recorder.Body.String()) + if body := strings.TrimSpace(recorder.Body.String()); body != "" && body != "{}" { + t.Fatalf("delete body = %q, want empty or {}", body) } if len(env.provisioner.deleted) != 1 || env.provisioner.deleted[0].Name != "vm01-root" { t.Fatalf("provisioner deleted = %+v", env.provisioner.deleted) diff --git a/internal/api/reconcile.go b/internal/api/reconcile.go index c0f42f0..97f1a68 100644 --- a/internal/api/reconcile.go +++ b/internal/api/reconcile.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/voidstarit/rookery/internal/qemu" "github.com/voidstarit/rookery/internal/store" ) @@ -95,6 +96,22 @@ func (s *Server) reconcileInfrastructure(ctx context.Context) error { // or a host reboot. VMs the user started come back even when QEMU died with // the host; VMs the user stopped stay down. It runs after the VPP resources are // reconciled so attached vhost-user sockets exist before QEMU launches. +func (s *Server) startVM(ctx context.Context, vm store.VM) error { + if s.startPlanner != nil { + plan, err := s.startPlanner.Plan(ctx, vm) + if err != nil { + return err + } + if starter, ok := s.vms.(qemu.PreparedStarter); ok { + return starter.StartWithPlan(ctx, vm, plan) + } + if len(plan.Attachments) > 0 { + return errors.New("VM runtime does not support prepared remote starts") + } + } + return s.vms.Start(ctx, vm) +} + func (s *Server) autostartVMs(ctx context.Context) { if s.vms == nil { return @@ -122,7 +139,7 @@ func (s *Server) autostartVMs(ctx context.Context) { s.logger.Warn().Str("vm_id", vm.ID).Str("vm", vm.Name).Msg("cannot auto-start VM without a disk") continue } - if err := s.vms.Start(ctx, vm); err != nil { + if err := s.startVM(ctx, vm); err != nil { s.logger.Error().Err(err).Str("vm_id", vm.ID).Str("vm", vm.Name).Msg("could not auto-start VM") if _, statusErr := s.store.SetVMStatus(ctx, vm.ID, store.StatusError); statusErr != nil { s.logger.Warn().Err(statusErr).Str("vm_id", vm.ID).Msg("could not mark VM as failed") diff --git a/internal/api/server.go b/internal/api/server.go index 8fe0b38..0573da8 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -47,6 +47,11 @@ type VMController interface { Metrics(vmID string) (qemu.Metrics, error) } +// StartPlanner prepares remote attachments for a VM start. +type StartPlanner interface { + Plan(ctx context.Context, vm store.VM) (qemu.StartPlan, error) +} + // LightVMController starts and stops QEMU processes for LightVM definitions. type LightVMController interface { StartLightVM(ctx context.Context, vm store.LightVM) error @@ -158,6 +163,7 @@ type Server struct { vppSocket string rookeryStartupOnce sync.Once vppStartupMu sync.Mutex + startPlanner StartPlanner // resourceMu serializes host provisioning (disks, bridge domains, vhost // interfaces) with VM attachment so resources cannot be removed mid-operation. @@ -198,6 +204,11 @@ func (s *Server) ConfigureLightVMRuntime(controller LightVMController) { s.lightvms = controller } +// ConfigureStartPlanner wires remote disk preparation into autostart. +func (s *Server) ConfigureStartPlanner(planner StartPlanner) { + s.startPlanner = planner +} + // SetVPPSocket records the VPP binary API socket exposed to network startup // scripts. It is optional and is passed through as ROOKERY_VPP_SOCKET. func (s *Server) SetVPPSocket(socket string) { diff --git a/internal/gateway/disk_kouja.go b/internal/gateway/disk_kouja.go new file mode 100644 index 0000000..ef9d8cd --- /dev/null +++ b/internal/gateway/disk_kouja.go @@ -0,0 +1,99 @@ +package gateway + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" + "github.com/voidstarit/rookery/internal/blockdev" + "github.com/voidstarit/rookery/internal/storage" + "github.com/voidstarit/rookery/internal/store" +) + +func (s *diskService) createKoujaDisk(ctx context.Context, request *rookeryv1.CreateDiskRequest, name string, backend store.StorageBackend, sizeGB int) (*rookeryv1.Disk, error) { + if request.GetIsTemplate() || request.TemplateDiskId != nil { + return nil, status.Error(codes.InvalidArgument, "kouja disks cannot be templates or template clones") + } + if strings.TrimSpace(request.GetProvisioningScript()) != "" || + strings.TrimSpace(request.GetProvisioningTimeout()) != "" || request.Provisioning != nil { + return nil, status.Error(codes.InvalidArgument, "kouja disks do not support provisioning settings") + } + if s.registry == nil { + return nil, status.Error(codes.Unavailable, "storage registry is not configured") + } + driver, ok := s.registry.Driver(backend.Driver) + if !ok { + return nil, status.Error(codes.Unavailable, "kouja storage driver is not configured") + } + volumeID := uuid.NewString() + requestID := "create-" + volumeID + protocol := blockdev.ProtocolISCSI + storageClass := "" + if backend.Config.Kouja != nil { + if strings.TrimSpace(backend.Config.Kouja.Protocol) == "nvme-tcp" { + protocol = blockdev.ProtocolNVMeTCP + } + storageClass = backend.Config.Kouja.StorageClass + } + disk := store.Disk{ + Name: name, SizeGB: sizeGB, + StorageBackendID: &backend.ID, StorageBackendName: backend.Name, + Source: store.DiskSourceKouja, Protocol: string(protocol), + BackendMetadata: store.DiskBackendMetadata{ + Version: store.DiskBackendMetadataVersion, Driver: store.DiskSourceKouja, + Volume: store.VolumeMetadata{VolumeID: volumeID, RequestID: requestID, StorageClass: storageClass}, + Attachment: blockdev.Reference{Protocol: protocol}, + }, + DeletionState: store.DiskDeletionProvisioning, + DeletionNextAttemptAt: time.Now().UTC().Add(20 * time.Minute), + } + + s.mu.Lock() + if _, err := s.store.GetDiskByName(ctx, name); err == nil { + s.mu.Unlock() + return nil, status.Error(codes.AlreadyExists, "a disk with that name already exists") + } else if !errors.Is(err, store.ErrDiskNotFound) { + s.mu.Unlock() + return nil, status.Error(codes.Internal, "could not check disk name") + } + created, err := s.store.CreateDisk(ctx, disk) + s.mu.Unlock() + if err != nil { + if errors.Is(err, store.ErrDiskNameTaken) { + return nil, status.Error(codes.AlreadyExists, "a disk with that name already exists") + } + return nil, status.Error(codes.Internal, "could not record disk") + } + + result, err := driver.CreateDisk(ctx, backend, storage.CreateDiskRequest{ + Disk: created, SizeBytes: int64(sizeGB) << 30, RequestID: requestID, + }) + if err != nil { + s.queueKoujaCleanup(ctx, created.ID) + return nil, status.Error(codes.Internal, "could not create kouja volume: "+err.Error()) + } + finalized, ok, err := s.store.FinalizeProvisioning(ctx, created.ID, result.Metadata, result.SizeBytes, result.Device, result.PreparedByScript) + if err != nil { + s.queueKoujaCleanup(ctx, created.ID) + return nil, status.Error(codes.Internal, "could not finalize kouja disk: "+err.Error()) + } + if !ok { + if current, readErr := s.store.GetDisk(ctx, created.ID); readErr == nil && current.DeletionState != "" { + return nil, status.Error(codes.Aborted, "disk was queued for cleanup while provisioning") + } + return nil, status.Error(codes.Internal, "could not finalize kouja disk") + } + return diskToProto(finalized), nil +} + +func (s *diskService) queueKoujaCleanup(ctx context.Context, id int64) { + if err := s.store.MarkDiskDeleting(ctx, id); err != nil && !errors.Is(err, store.ErrDiskNotFound) { + s.logger.Warn().Err(err).Int64("disk_id", id).Msg("could not queue kouja cleanup") + } +} diff --git a/internal/gateway/disk_kouja_test.go b/internal/gateway/disk_kouja_test.go new file mode 100644 index 0000000..d4acdab --- /dev/null +++ b/internal/gateway/disk_kouja_test.go @@ -0,0 +1,152 @@ +package gateway_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/voidstarit/rookery/internal/blockdev" + "github.com/voidstarit/rookery/internal/gateway" + "github.com/voidstarit/rookery/internal/storage" + "github.com/voidstarit/rookery/internal/store" +) + +type gatewayKoujaDriver struct { + prepared int +} + +func (d *gatewayKoujaDriver) Kind() string { return store.DiskSourceKouja } +func (d *gatewayKoujaDriver) CreateDisk(_ context.Context, _ store.StorageBackend, request storage.CreateDiskRequest) (storage.CreatedDisk, error) { + return storage.CreatedDisk{ + Metadata: store.DiskBackendMetadata{ + Version: 1, Driver: store.DiskSourceKouja, + Volume: store.VolumeMetadata{VolumeID: request.Disk.BackendMetadata.Volume.VolumeID, RequestID: request.RequestID, + ResourceID: "resource-1", Owner: "spiffe://kouja/caller/rookery", StorageClass: "nvme"}, + Attachment: blockdev.Reference{Protocol: blockdev.ProtocolISCSI, ISCSI: &blockdev.ISCSIReference{ + Portal: "10.0.0.1:3260", ExportPortal: "10.0.0.1:3260", + Target: "iqn.2026-11.io.example:disk", LUN: 0, Initiator: "iqn.2026-11.io.example:rookery", + }}, + }, + SizeBytes: 5 << 30, + }, nil +} +func (d *gatewayKoujaDriver) DeleteDisk(context.Context, store.StorageBackend, store.Disk) error { + return nil +} +func (d *gatewayKoujaDriver) Health(context.Context, store.StorageBackend, *store.Disk) (storage.Health, error) { + return storage.Health{Reachable: true, Ready: true}, nil +} +func (d *gatewayKoujaDriver) PrepareStart(context.Context, store.StorageBackend, store.Disk) (blockdev.Attachment, error) { + d.prepared++ + return blockdev.Attachment{ + Protocol: blockdev.ProtocolISCSI, + ISCSI: &blockdev.ISCSIAttachment{ + ISCSIReference: blockdev.ISCSIReference{Portal: "10.0.0.1:3260", Target: "iqn.2026-11.io.example:disk", LUN: 0}, + Username: "kouja-resource-1", Credential: "secret", + }, + }, nil +} + +func TestKoujaDiskAndVMStartRoutes(t *testing.T) { + ctx := context.Background() + st, err := store.Open(filepath.Join(t.TempDir(), "kouja-gateway.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + backend, err := st.CreateStorageBackend(ctx, store.StorageBackend{ + Name: "kouja", Driver: store.DiskSourceKouja, + Config: store.StorageBackendConfig{Kouja: &store.KoujaConfig{StorageClass: "nvme"}}, + }) + if err != nil { + t.Fatal(err) + } + driver := &gatewayKoujaDriver{} + registry := storage.NewRegistry(driver) + runtime := &fakeVMController{} + gw, err := gateway.New(st, fakeReadiness{status: readyStatus()}, + gateway.WithStorageRegistry(registry), gateway.WithVMController(runtime)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(gw.Close) + server := httptest.NewServer(gw.Handler) + t.Cleanup(server.Close) + + body := strings.NewReader(`{"name":"remote-disk","size_gb":5,"storage_backend_id":` + jsonNumber(backend.ID) + `}`) + response, err := http.Post(server.URL+"/api/disks", "application/json", body) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusCreated { + t.Fatalf("create status = %d, body = %s", response.StatusCode, readBody(t, response)) + } + var disk map[string]any + decodeBody(t, response, &disk) + id := jsonID(disk["id"]) + if disk["source"] != "kouja" || disk["protocol"] != "iscsi" || !strings.Contains(disk["locator"].(string), "iqn.2026-11.io.example:disk") { + t.Fatalf("disk = %+v", disk) + } + + vmBody := strings.NewReader(`{"name":"remote-vm","disk_ids":[` + id + `]}`) + vmResponse, err := http.Post(server.URL+"/api/vms", "application/json", vmBody) + if err != nil { + t.Fatal(err) + } + if vmResponse.StatusCode != http.StatusCreated { + t.Fatalf("create vm status = %d, body = %s", vmResponse.StatusCode, readBody(t, vmResponse)) + } + if driver.prepared != 1 || len(runtime.plan.Attachments) != 1 { + t.Fatalf("prepared=%d plan=%+v", driver.prepared, runtime.plan) + } + var vm map[string]any + decodeBody(t, vmResponse, &vm) + vmID := vm["id"].(string) + + stop, err := http.Post(server.URL+"/api/vms/"+vmID+"/stop", "application/json", nil) + if err != nil { + t.Fatal(err) + } + _ = stop.Body.Close() + deleteVM, err := http.NewRequest(http.MethodDelete, server.URL+"/api/vms/"+vmID, nil) + if err != nil { + t.Fatal(err) + } + deletedVM, err := http.DefaultClient.Do(deleteVM) + if err != nil { + t.Fatal(err) + } + _ = deletedVM.Body.Close() + + deleteDisk, err := http.NewRequest(http.MethodDelete, server.URL+"/api/disks/"+id, nil) + if err != nil { + t.Fatal(err) + } + deletedDisk, err := http.DefaultClient.Do(deleteDisk) + if err != nil { + t.Fatal(err) + } + if deletedDisk.StatusCode != http.StatusAccepted { + t.Fatalf("delete disk status = %d, body = %s", deletedDisk.StatusCode, readBody(t, deletedDisk)) + } + _ = deletedDisk.Body.Close() + stored, err := st.GetDisk(ctx, jsonIDInt(id)) + if err != nil || stored.DeletionState != store.DiskDeletionDeleting { + t.Fatalf("stored disk = %+v, %v", stored, err) + } +} + +func jsonNumber(value int64) string { + data, _ := json.Marshal(value) + return string(data) +} + +func jsonIDInt(value string) int64 { + var id int64 + _ = json.Unmarshal([]byte(value), &id) + return id +} diff --git a/internal/gateway/disk_writes.go b/internal/gateway/disk_writes.go index d6e4f26..ed943ca 100644 --- a/internal/gateway/disk_writes.go +++ b/internal/gateway/disk_writes.go @@ -11,7 +11,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" "github.com/voidstarit/rookery/internal/host" @@ -46,6 +45,16 @@ func (s *diskService) CreateDisk(ctx context.Context, request *rookeryv1.CreateD if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + if backend.Driver == store.DiskSourceKouja { + sizeGB := int(request.GetSizeGb()) + if sizeGB == 0 { + sizeGB = defaultGatewayDiskGB + } + if sizeGB < 1 || sizeGB > maxGatewayDiskGB { + return nil, status.Error(codes.InvalidArgument, "size_gb is out of range") + } + return s.createKoujaDisk(ctx, request, name, backend, sizeGB) + } if backend.Driver != "lvm-thin" || backend.Config.VG == "" || backend.Config.ThinPool == "" { return nil, status.Error(codes.FailedPrecondition, "selected storage backend is not a configured LVM thin backend") } @@ -377,7 +386,7 @@ func diskUpdateError(err error) error { } } -func (s *diskService) DeleteDisk(ctx context.Context, request *rookeryv1.DeleteDiskRequest) (*emptypb.Empty, error) { +func (s *diskService) DeleteDisk(ctx context.Context, request *rookeryv1.DeleteDiskRequest) (*rookeryv1.DeleteDiskResponse, error) { s.mu.Lock() defer s.mu.Unlock() disk, err := s.store.GetDisk(ctx, request.GetId()) @@ -387,6 +396,15 @@ func (s *diskService) DeleteDisk(ctx context.Context, request *rookeryv1.DeleteD if disk.AttachedVMID != nil { return nil, status.Error(codes.Aborted, "disk is attached to a VM; delete the VM first") } + if disk.Source == store.DiskSourceKouja { + if s.registry == nil { + return nil, status.Error(codes.Unavailable, "storage registry is not configured") + } + if err := s.store.MarkDiskDeleting(ctx, disk.ID); err != nil { + return nil, status.Error(codes.Internal, "could not queue disk deletion") + } + return &rookeryv1.DeleteDiskResponse{Accepted: true, State: store.DiskDeletionDeleting}, nil + } if disk.Source == store.DiskSourceHostDevice { if err := s.store.DeleteDisk(ctx, request.GetId()); err != nil { switch { @@ -398,7 +416,7 @@ func (s *diskService) DeleteDisk(ctx context.Context, request *rookeryv1.DeleteD return nil, status.Error(codes.Internal, "could not delete disk") } } - return &emptypb.Empty{}, nil + return &rookeryv1.DeleteDiskResponse{}, nil } backend, backendErr := s.resolveStorageBackend(ctx, disk.StorageBackendID) if disk.IsTemplate { @@ -432,7 +450,7 @@ func (s *diskService) DeleteDisk(ctx context.Context, request *rookeryv1.DeleteD return nil, status.Error(codes.Internal, "could not delete disk") } } - return &emptypb.Empty{}, nil + return &rookeryv1.DeleteDiskResponse{}, nil } func (s *diskService) resolveStorageBackend(ctx context.Context, id *int64) (store.StorageBackend, error) { @@ -462,6 +480,9 @@ func (s *diskService) resolveStorageBackend(ctx context.Context, id *int64) (sto return candidate, nil } } + if len(backends) == 1 { + return backends[0], nil + } if s.readiness == nil { return store.StorageBackend{}, errors.New("no storage backend selected") } diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index ce7ad34..6fc9bfb 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -25,7 +25,9 @@ import ( rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" "github.com/voidstarit/rookery/internal/readiness" + "github.com/voidstarit/rookery/internal/storage" "github.com/voidstarit/rookery/internal/store" + "github.com/voidstarit/rookery/internal/vmstart" ) // Readiness supplies the current host readiness state. @@ -73,6 +75,8 @@ type options struct { cgroup CgroupRuntime lightvmDir string lightvmRuntime LightVMRuntime + storageRegistry *storage.Registry + startPlanner *vmstart.Planner } // WithGRPCListener exposes the generated gRPC server on listener instead of an @@ -123,6 +127,20 @@ func WithDiskProvisioner(provisioner DiskProvisioner) Option { } } +// WithStorageRegistry sets the storage driver registry used for remote disks. +func WithStorageRegistry(registry *storage.Registry) Option { + return func(opts *options) { + opts.storageRegistry = registry + } +} + +// WithStartPlanner sets the VM start planner used for remote attachments. +func WithStartPlanner(planner *vmstart.Planner) Option { + return func(opts *options) { + opts.startPlanner = planner + } +} + // WithHostDeviceValidator overrides host block-device validation. Tests use it // to exercise passthrough disk registration without a real block device. func WithHostDeviceValidator(validator HostDeviceValidator) Option { @@ -275,6 +293,12 @@ func New(st *store.Store, ready Readiness, opts ...Option) (*Gateway, error) { if options.vmMutex == nil { options.vmMutex = &sync.Mutex{} } + if options.storageRegistry == nil { + options.storageRegistry = storage.NewDefaultRegistry() + } + if options.startPlanner == nil { + options.startPlanner = vmstart.NewPlanner(st, options.storageRegistry) + } listener := options.grpcListener useBufconn := listener == nil if useBufconn { @@ -292,15 +316,15 @@ func New(st *store.Store, ready Readiness, opts ...Option) (*Gateway, error) { if provider, ok := options.lightvmRuntime.(LightVMStatusProvider); ok { system.lightvmStatus = provider } - vms := &vmService{store: st, runtime: options.vmController, counters: options.interfaceCounters, readiness: ready, resourceMu: options.diskMutex, mu: options.vmMutex, logger: options.logger} - disks := &diskService{store: st, runtime: options.diskProvisioner, readiness: ready, mu: options.diskMutex, logger: options.logger, hostDevices: options.hostDevices} + vms := &vmService{store: st, runtime: options.vmController, counters: options.interfaceCounters, readiness: ready, resourceMu: options.diskMutex, mu: options.vmMutex, logger: options.logger, planner: options.startPlanner} + disks := &diskService{store: st, runtime: options.diskProvisioner, readiness: ready, mu: options.diskMutex, logger: options.logger, hostDevices: options.hostDevices, registry: options.storageRegistry} media := &mediaService{store: st} networks := &networkService{store: st, runtime: options.networkRuntime, readiness: ready, writer: options.networkWriter, mu: options.diskMutex} interfaces := &interfaceService{store: st, runtime: options.aclRuntime, attachments: options.interfaceRuntime, addresses: options.interfaceAddresses, vms: options.vmController, counters: options.interfaceCounters, readiness: ready, resourceMu: options.diskMutex, runMu: options.vmMutex} acls := &aclService{store: st, runtime: options.aclRuntime, readiness: ready} routeTables := &routeTableService{store: st, runtime: options.aclRuntime, readiness: ready} vnfs := &vnfService{store: st, runtime: options.vnfRuntime, statusProvider: options.vnfStatusProvider, writer: options.vnfWriter} - backends := &storageBackendService{store: st, readiness: ready, disks: options.diskProvisioner, mu: options.diskMutex} + backends := &storageBackendService{store: st, readiness: ready, disks: options.diskProvisioner, mu: options.diskMutex, registry: options.storageRegistry} hostInterfaces := &hostService{source: options.vppInterfaces, store: st, writer: options.hostWriter, taps: options.hostTapWriter, devices: options.hostDeviceLister, addresses: options.interfaceAddresses, readiness: ready} troubleshoot := &troubleshootService{runtime: options.diagnostics, readiness: ready} captures := &captureService{runtime: options.diagnostics, readiness: ready} @@ -560,6 +584,7 @@ type vmService struct { resourceMu *sync.Mutex mu *sync.Mutex logger zerolog.Logger + planner *vmstart.Planner } func (s *vmService) ListVMs(ctx context.Context, _ *rookeryv1.ListVMsRequest) (*rookeryv1.ListVMsResponse, error) { @@ -632,5 +657,6 @@ func vmDiskToProto(disk store.VMDisk) *rookeryv1.VMDisk { Id: disk.ID, Name: disk.Name, SizeGb: int32(disk.SizeGB), SizeBytes: disk.SizeBytes, StorageBackendId: disk.StorageBackendID, StorageBackendName: disk.StorageBackendName, Vg: disk.VG, ThinPool: disk.ThinPool, Device: disk.Device, Source: disk.Source, + Protocol: disk.Protocol, } } diff --git a/internal/gateway/http_status.go b/internal/gateway/http_status.go index 9e43ab2..068a947 100644 --- a/internal/gateway/http_status.go +++ b/internal/gateway/http_status.go @@ -33,7 +33,6 @@ var createdMethods = map[string]bool{ var noContentMethods = map[string]bool{ "/rookery.v1.VMService/DeleteVM": true, - "/rookery.v1.DiskService/DeleteDisk": true, "/rookery.v1.MediaService/DeleteMedia": true, "/rookery.v1.NetworkService/DeleteNetwork": true, "/rookery.v1.NetworkService/DeleteNetworkPipe": true, @@ -97,6 +96,15 @@ func httpResponseStatus(ctx context.Context, writer http.ResponseWriter, respons if !ok { return nil } + switch value := response.(type) { + case *rookeryv1.DeleteDiskResponse: + if value.GetAccepted() { + writer.WriteHeader(http.StatusAccepted) + } else { + writer.WriteHeader(http.StatusNoContent) + } + return nil + } switch { case createdMethods[method]: setLocationHeader(method, response, writer) diff --git a/internal/gateway/readonly.go b/internal/gateway/readonly.go index 3bbe12d..319a932 100644 --- a/internal/gateway/readonly.go +++ b/internal/gateway/readonly.go @@ -12,6 +12,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" + "github.com/voidstarit/rookery/internal/storage" "github.com/voidstarit/rookery/internal/store" ) @@ -37,6 +38,7 @@ type diskService struct { mu *sync.Mutex logger zerolog.Logger hostDevices HostDeviceValidator + registry *storage.Registry } func (s *diskService) ListDisks(ctx context.Context, _ *rookeryv1.ListDisksRequest) (*rookeryv1.ListDisksResponse, error) { @@ -67,6 +69,8 @@ func diskToProto(disk store.Disk) *rookeryv1.Disk { IsTemplate: disk.IsTemplate, TemplateDiskId: disk.TemplateDiskID, TemplateDiskName: disk.TemplateDiskName, ProvisioningScript: disk.ProvisioningScript, ProvisioningTimeout: disk.ProvisioningTimeout, AttachedVmId: disk.AttachedVMID, AttachedVmName: disk.AttachedVMName, + Locator: disk.BackendMetadata.Attachment.Locator(), Protocol: disk.Protocol, + DeletionState: disk.DeletionState, DeletionError: disk.DeletionError, CreatedAt: timestamp(disk.CreatedAt), UpdatedAt: timestamp(disk.UpdatedAt), } } @@ -378,6 +382,7 @@ type storageBackendService struct { readiness Readiness disks DiskProvisioner mu *sync.Mutex + registry *storage.Registry } func (s *storageBackendService) ListStorageBackends(ctx context.Context, _ *rookeryv1.ListStorageBackendsRequest) (*rookeryv1.ListStorageBackendsResponse, error) { @@ -400,17 +405,30 @@ func (s *storageBackendService) GetStorageBackend(ctx context.Context, request * return storageBackendToProto(backend), nil } +func backendConfigToProto(config store.StorageBackendConfig) *rookeryv1.StorageBackendConfig { + result := &rookeryv1.StorageBackendConfig{ + Vg: config.VG, ThinPool: config.ThinPool, + SupportsTemplates: config.SupportsTemplates, + PreparationScript: config.PreparationScript, + PreparationTimeout: config.PreparationTimeout, + AllowTemplateDeleteWithClones: config.AllowTemplateDeleteWithClones, + } + if config.Kouja != nil { + result.DriverConfig = &rookeryv1.StorageBackendConfig_Kouja{Kouja: &rookeryv1.KoujaConfig{ + Endpoint: config.Kouja.Endpoint, Protocol: config.Kouja.Protocol, + StorageClass: config.Kouja.StorageClass, Initiator: config.Kouja.Initiator, + HostNqn: config.Kouja.HostNQN, CaFile: config.Kouja.CAFile, + ClientCertFile: config.Kouja.ClientCertFile, ClientKeyFile: config.Kouja.ClientKeyFile, + ServerName: config.Kouja.ServerName, Portal: config.Kouja.Portal, + }} + } + return result +} func storageBackendToProto(backend store.StorageBackend) *rookeryv1.StorageBackend { return &rookeryv1.StorageBackend{ Id: backend.ID, Name: backend.Name, Title: backend.Title, Description: backend.Description, - Driver: backend.Driver, - Config: &rookeryv1.StorageBackendConfig{ - Vg: backend.Config.VG, ThinPool: backend.Config.ThinPool, - SupportsTemplates: backend.Config.SupportsTemplates, - PreparationScript: backend.Config.PreparationScript, - PreparationTimeout: backend.Config.PreparationTimeout, - AllowTemplateDeleteWithClones: backend.Config.AllowTemplateDeleteWithClones, - }, + Driver: backend.Driver, + Config: backendConfigToProto(backend.Config), DiskCount: int32(backend.DiskCount), CreatedAt: timestamp(backend.CreatedAt), UpdatedAt: timestamp(backend.UpdatedAt), } diff --git a/internal/gateway/storage_backend_health.go b/internal/gateway/storage_backend_health.go new file mode 100644 index 0000000..a99b8b6 --- /dev/null +++ b/internal/gateway/storage_backend_health.go @@ -0,0 +1,84 @@ +package gateway + +import ( + "context" + "errors" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" + "github.com/voidstarit/rookery/internal/store" +) + +func (s *storageBackendService) GetStorageBackendHealth(ctx context.Context, request *rookeryv1.GetStorageBackendHealthRequest) (*rookeryv1.StorageBackendHealth, error) { + backend, err := s.store.GetStorageBackend(ctx, request.GetId()) + if err != nil { + return nil, storeReadError(err, store.ErrStorageBackendNotFound, "storage backend not found") + } + health := &rookeryv1.StorageBackendHealth{Id: backend.ID, Driver: backend.Driver, QemuReady: false} + if backend.Driver == "lvm-thin" { + if s.readiness != nil { + status := s.readiness.Status() + health.Reachable = true + health.Ready = status.ThinPool.Ready + health.Detail = status.ThinPool.Detail + health.CheckedAt = timestamp(time.Now().UTC()) + } + return s.withDeletionBacklog(ctx, health) + } + driver, ok := s.registry.Driver(backend.Driver) + if !ok { + health.Detail = "storage driver is not available" + return s.withDeletionBacklog(ctx, health) + } + var sample *store.Disk + disks, listErr := s.store.ListDisks(ctx) + if listErr == nil { + for i := range disks { + if disks[i].StorageBackendID != nil && *disks[i].StorageBackendID == backend.ID { + sample = &disks[i] + break + } + } + } + result, healthErr := driver.Health(ctx, backend, sample) + if healthErr != nil { + health.Detail = healthErr.Error() + return s.withDeletionBacklog(ctx, health) + } + health.Reachable = result.Reachable + health.Ready = result.Ready + health.Detail = result.Detail + health.Protocols = result.Protocols + health.CheckedAt = timestamp(result.CheckedAt) + if backend.Config.Kouja != nil { + health.StorageClass = backend.Config.Kouja.StorageClass + } + for _, class := range result.Classes { + health.Classes = append(health.Classes, &rookeryv1.StorageClassCapacity{ + Name: class.Name, VgName: class.VGName, State: class.State, + TotalBytes: class.TotalBytes, FreeBytes: class.FreeBytes, + AllocatableBytes: class.AllocatableBytes, MaxVolumeBytes: class.MaxVolumeBytes, + }) + } + return s.withDeletionBacklog(ctx, health) +} + +func (s *storageBackendService) TestStorageBackendDataPath(context.Context, *rookeryv1.TestStorageBackendDataPathRequest) (*rookeryv1.StorageBackendHealth, error) { + return nil, status.Error(codes.Unimplemented, "deep storage data-path testing is not implemented yet") +} + +func (s *storageBackendService) withDeletionBacklog(ctx context.Context, health *rookeryv1.StorageBackendHealth) (*rookeryv1.StorageBackendHealth, error) { + if s.store == nil { + return health, nil + } + count, age, err := s.store.DueDiskDeletionCount(ctx, time.Now().UTC()) + if err != nil && !errors.Is(err, store.ErrNotFound) { + return health, nil + } + health.PendingDeletions = count + health.OldestPendingDeletionAgeSeconds = int64(age / time.Second) + return health, nil +} diff --git a/internal/gateway/vm_actions.go b/internal/gateway/vm_actions.go index 03286cd..5eeda17 100644 --- a/internal/gateway/vm_actions.go +++ b/internal/gateway/vm_actions.go @@ -38,6 +38,22 @@ type InterfaceCounterSource interface { ResetInterfaceCounters(ctx context.Context) error } +func (s *vmService) startVM(ctx context.Context, vm store.VM) error { + if s.planner != nil { + plan, err := s.planner.Plan(ctx, vm) + if err != nil { + return err + } + if starter, ok := s.runtime.(qemu.PreparedStarter); ok { + return starter.StartWithPlan(ctx, vm, plan) + } + if len(plan.Attachments) > 0 { + return errors.New("VM runtime does not support prepared remote starts") + } + } + return s.runtime.Start(ctx, vm) +} + func (s *vmService) runtimeUnavailable() error { if s.runtime == nil { return status.Error(codes.Unavailable, "VM runtime is not configured") @@ -73,7 +89,7 @@ func (s *vmService) StartVM(ctx context.Context, request *rookeryv1.StartVMReque } return nil, status.Error(codes.AlreadyExists, "vm is already running") } - if err := s.runtime.Start(ctx, vm); err != nil { + if err := s.startVM(ctx, vm); err != nil { _, _ = s.store.SetVMStatus(ctx, vm.ID, store.StatusError) return nil, status.Error(codes.Internal, "could not start VM: "+err.Error()) } @@ -137,7 +153,7 @@ func (s *vmService) ResetVM(ctx context.Context, request *rookeryv1.ResetVMReque if stopErr := s.runtime.Stop(ctx, vm.ID); stopErr != nil { return nil, status.Error(codes.Internal, "could not stop VM for reset: "+stopErr.Error()) } - if startErr := s.runtime.Start(ctx, vm); startErr != nil { + if startErr := s.startVM(ctx, vm); startErr != nil { _, _ = s.store.SetVMStatus(ctx, vm.ID, store.StatusError) return nil, status.Error(codes.Internal, "could not restart VM: "+startErr.Error()) } diff --git a/internal/gateway/vm_actions_test.go b/internal/gateway/vm_actions_test.go index eda33be..2d9bb3e 100644 --- a/internal/gateway/vm_actions_test.go +++ b/internal/gateway/vm_actions_test.go @@ -23,6 +23,7 @@ type fakeVMController struct { startErr error cgroupErr error cgroupApplied []store.VM + plan qemu.StartPlan } func (f *fakeVMController) Start(_ context.Context, _ store.VM) error { @@ -32,6 +33,10 @@ func (f *fakeVMController) Start(_ context.Context, _ store.VM) error { f.running = true return nil } +func (f *fakeVMController) StartWithPlan(ctx context.Context, vm store.VM, plan qemu.StartPlan) error { + f.plan = plan + return f.Start(ctx, vm) +} func (f *fakeVMController) Stop(_ context.Context, _ string) error { f.running = false return nil diff --git a/internal/gateway/vm_create.go b/internal/gateway/vm_create.go index a1c8bf7..2de666e 100644 --- a/internal/gateway/vm_create.go +++ b/internal/gateway/vm_create.go @@ -97,7 +97,7 @@ func (s *vmService) startCreatedVM(ctx context.Context, vm store.VM) (store.VM, } s.mu.Lock() defer s.mu.Unlock() - if err := s.runtime.Start(ctx, vm); err != nil { + if err := s.startVM(ctx, vm); err != nil { s.rollbackCreatedVM(vm.ID) return store.VM{}, status.Error(codes.Internal, "could not start VM: "+err.Error()) } diff --git a/internal/gateway/writes.go b/internal/gateway/writes.go index c02348c..a303867 100644 --- a/internal/gateway/writes.go +++ b/internal/gateway/writes.go @@ -16,6 +16,7 @@ import ( rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" "github.com/voidstarit/rookery/internal/host" + "github.com/voidstarit/rookery/internal/kouja" "github.com/voidstarit/rookery/internal/store" ) @@ -111,6 +112,24 @@ func normalizeStorageBackendFields(name, title, description, driver string, conf if driver == "" { driver = "lvm-thin" } + if driver == "kouja" { + if config == nil || config.GetKouja() == nil { + return store.StorageBackend{}, errors.New("kouja configuration is required") + } + raw := config.GetKouja() + cfg := &store.KoujaConfig{ + Endpoint: strings.TrimSpace(raw.GetEndpoint()), Protocol: strings.TrimSpace(raw.GetProtocol()), + StorageClass: strings.TrimSpace(raw.GetStorageClass()), Initiator: strings.TrimSpace(raw.GetInitiator()), + HostNQN: strings.TrimSpace(raw.GetHostNqn()), CAFile: strings.TrimSpace(raw.GetCaFile()), + ClientCertFile: strings.TrimSpace(raw.GetClientCertFile()), ClientKeyFile: strings.TrimSpace(raw.GetClientKeyFile()), + ServerName: strings.TrimSpace(raw.GetServerName()), Portal: strings.TrimSpace(raw.GetPortal()), + } + if _, err := kouja.ParseConfig(store.StorageBackendConfig{Kouja: cfg}); err != nil { + return store.StorageBackend{}, err + } + return store.StorageBackend{Name: name, Title: title, Description: description, Driver: driver, + Config: store.StorageBackendConfig{Kouja: cfg}}, nil + } if driver != "lvm-thin" { return store.StorageBackend{}, fmt.Errorf("unsupported storage driver %q", driver) } From b869b5ccf6570db93f8278f31c511b6e2963f885 Mon Sep 17 00:00:00 2001 From: Masoud Mazarei Date: Sat, 7 Nov 2026 12:30:54 -0500 Subject: [PATCH 2/3] test(gateway): expand phase 4 kouja routing coverage --- internal/gateway/disk_kouja_test.go | 193 +++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) diff --git a/internal/gateway/disk_kouja_test.go b/internal/gateway/disk_kouja_test.go index d4acdab..3a6c366 100644 --- a/internal/gateway/disk_kouja_test.go +++ b/internal/gateway/disk_kouja_test.go @@ -2,12 +2,22 @@ package gateway_test import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" + "errors" + "math/big" "net/http" "net/http/httptest" + "net/url" + "os" "path/filepath" "strings" "testing" + "time" "github.com/voidstarit/rookery/internal/blockdev" "github.com/voidstarit/rookery/internal/gateway" @@ -16,11 +26,16 @@ import ( ) type gatewayKoujaDriver struct { - prepared int + prepared int + createErr error + healthErr error } func (d *gatewayKoujaDriver) Kind() string { return store.DiskSourceKouja } func (d *gatewayKoujaDriver) CreateDisk(_ context.Context, _ store.StorageBackend, request storage.CreateDiskRequest) (storage.CreatedDisk, error) { + if d.createErr != nil { + return storage.CreatedDisk{}, d.createErr + } return storage.CreatedDisk{ Metadata: store.DiskBackendMetadata{ Version: 1, Driver: store.DiskSourceKouja, @@ -38,7 +53,11 @@ func (d *gatewayKoujaDriver) DeleteDisk(context.Context, store.StorageBackend, s return nil } func (d *gatewayKoujaDriver) Health(context.Context, store.StorageBackend, *store.Disk) (storage.Health, error) { - return storage.Health{Reachable: true, Ready: true}, nil + if d.healthErr != nil { + return storage.Health{}, d.healthErr + } + return storage.Health{Reachable: true, Ready: true, Protocols: []string{"iscsi"}, + Classes: []storage.ClassCapacity{{Name: "nvme", VGName: "vg0", State: "ready", AllocatableBytes: 10}}}, nil } func (d *gatewayKoujaDriver) PrepareStart(context.Context, store.StorageBackend, store.Disk) (blockdev.Attachment, error) { d.prepared++ @@ -150,3 +169,173 @@ func jsonIDInt(value string) int64 { _ = json.Unmarshal([]byte(value), &id) return id } + +func gatewayCert(t *testing.T) (string, string, string) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + uri, _ := url.Parse("spiffe://kouja/caller/rookery") + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "rookery"}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, IsCA: true, + URIs: []*url.URL{uri}, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + certFile, keyFile, caFile := filepath.Join(dir, "client.crt"), filepath.Join(dir, "client.key"), filepath.Join(dir, "ca.crt") + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + if err := os.WriteFile(certFile, certPEM, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(caFile, certPEM, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600); err != nil { + t.Fatal(err) + } + return certFile, keyFile, caFile +} + +func koujaBackendBody(t *testing.T, name string) string { + t.Helper() + certFile, keyFile, caFile := gatewayCert(t) + return `{"name":"` + name + `","driver":"kouja","config":{"kouja":{` + + `"endpoint":"10.0.0.1:9443","protocol":"iscsi","storage_class":"nvme",` + + `"initiator":"iqn.2026-11.io.example:rookery","ca_file":"` + caFile + `",` + + `"client_cert_file":"` + certFile + `","client_key_file":"` + keyFile + `"}}}` +} + +func TestKoujaBackendConfigAndHealthRoutes(t *testing.T) { + ctx := context.Background() + st, err := store.Open(filepath.Join(t.TempDir(), "kouja-backend.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + driver := &gatewayKoujaDriver{} + registry := storage.NewRegistry(driver) + gw, err := gateway.New(st, fakeReadiness{status: readyStatus()}, gateway.WithStorageRegistry(registry)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(gw.Close) + server := httptest.NewServer(gw.Handler) + t.Cleanup(server.Close) + + response, err := http.Post(server.URL+"/api/storage-backends", "application/json", strings.NewReader(koujaBackendBody(t, "kouja-backend"))) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusCreated { + t.Fatalf("backend create = %d, %s", response.StatusCode, readBody(t, response)) + } + var backend map[string]any + decodeBody(t, response, &backend) + id := jsonID(backend["id"]) + config := backend["config"].(map[string]any)["kouja"].(map[string]any) + if config["storage_class"] != "nvme" { + t.Fatalf("config = %+v", backend["config"]) + } + + health, err := http.Get(server.URL + "/api/storage-backends/" + id + "/health") + if err != nil { + t.Fatal(err) + } + if health.StatusCode != http.StatusOK { + t.Fatalf("health = %d, %s", health.StatusCode, readBody(t, health)) + } + var healthBody map[string]any + decodeBody(t, health, &healthBody) + if healthBody["ready"] != true || len(healthBody["classes"].([]any)) != 1 { + t.Fatalf("health body = %+v", healthBody) + } + + _ = st.DeleteStorageBackend(ctx, jsonIDInt(id)) +} + +func TestKoujaDiskValidationAndCreateFailure(t *testing.T) { + ctx := context.Background() + st, err := store.Open(filepath.Join(t.TempDir(), "kouja-disk-errors.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + backend, err := st.CreateStorageBackend(ctx, store.StorageBackend{Name: "kouja", Driver: store.DiskSourceKouja}) + if err != nil { + t.Fatal(err) + } + driver := &gatewayKoujaDriver{createErr: errors.New("boom")} + gw, err := gateway.New(st, fakeReadiness{status: readyStatus()}, gateway.WithStorageRegistry(storage.NewRegistry(driver))) + if err != nil { + t.Fatal(err) + } + t.Cleanup(gw.Close) + server := httptest.NewServer(gw.Handler) + t.Cleanup(server.Close) + post := func(body string) *http.Response { + t.Helper() + response, err := http.Post(server.URL+"/api/disks", "application/json", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + return response + } + for _, body := range []string{ + `{"name":"template","size_gb":5,"storage_backend_id":` + jsonNumber(backend.ID) + `,"is_template":true}`, + `{"name":"provisioned","size_gb":5,"storage_backend_id":` + jsonNumber(backend.ID) + `,"provisioning_script":"/bin/true"}`, + } { + response := post(body) + if response.StatusCode != http.StatusBadRequest { + t.Fatalf("validation status = %d, %s", response.StatusCode, readBody(t, response)) + } + _ = response.Body.Close() + } + response := post(`{"name":"failure","size_gb":5,"storage_backend_id":` + jsonNumber(backend.ID) + `}`) + if response.StatusCode != http.StatusInternalServerError { + t.Fatalf("create failure = %d, %s", response.StatusCode, readBody(t, response)) + } + _ = response.Body.Close() + failed, err := st.GetDiskByName(ctx, "failure") + if err != nil || failed.DeletionState != store.DiskDeletionDeleting { + t.Fatalf("failed disk = %+v, %v", failed, err) + } +} + +func TestKoujaOnlyBackendSelection(t *testing.T) { + ctx := context.Background() + st, err := store.Open(filepath.Join(t.TempDir(), "kouja-select.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + if _, err := st.CreateStorageBackend(ctx, store.StorageBackend{Name: "only", Driver: store.DiskSourceKouja, Config: store.StorageBackendConfig{Kouja: &store.KoujaConfig{StorageClass: "nvme"}}}); err != nil { + t.Fatal(err) + } + driver := &gatewayKoujaDriver{} + gw, err := gateway.New(st, fakeReadiness{status: readyStatus()}, gateway.WithStorageRegistry(storage.NewRegistry(driver))) + if err != nil { + t.Fatal(err) + } + t.Cleanup(gw.Close) + server := httptest.NewServer(gw.Handler) + t.Cleanup(server.Close) + response, err := http.Post(server.URL+"/api/disks", "application/json", strings.NewReader(`{"name":"implicit","size_gb":5}`)) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusCreated { + t.Fatalf("implicit backend = %d, %s", response.StatusCode, readBody(t, response)) + } + _ = response.Body.Close() +} From 101ab4c73f4766cd320452c20ae8ad58ed45b531 Mon Sep 17 00:00:00 2001 From: Masoud Mazarei Date: Sat, 7 Nov 2026 12:35:20 -0500 Subject: [PATCH 3/3] test(gateway,api): raise phase 4 branch coverage --- internal/api/start_planner_internal_test.go | 77 ++++++ .../gateway/kouja_branches_internal_test.go | 242 ++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 internal/api/start_planner_internal_test.go create mode 100644 internal/gateway/kouja_branches_internal_test.go diff --git a/internal/api/start_planner_internal_test.go b/internal/api/start_planner_internal_test.go new file mode 100644 index 0000000..d970342 --- /dev/null +++ b/internal/api/start_planner_internal_test.go @@ -0,0 +1,77 @@ +package api + +import ( + "context" + "errors" + "testing" + + "github.com/voidstarit/rookery/internal/blockdev" + "github.com/voidstarit/rookery/internal/qemu" + "github.com/voidstarit/rookery/internal/store" +) + +type plannerStub struct { + plan qemu.StartPlan + err error +} + +func (p *plannerStub) Plan(context.Context, store.VM) (qemu.StartPlan, error) { + return p.plan, p.err +} + +type plannerRuntime struct { + started int + startedPlan int + lastPlan qemu.StartPlan +} + +func (r *plannerRuntime) Start(context.Context, store.VM) error { r.started++; return nil } +func (r *plannerRuntime) StartWithPlan(_ context.Context, _ store.VM, plan qemu.StartPlan) error { + r.startedPlan++ + r.lastPlan = plan + return nil +} +func (r *plannerRuntime) Stop(context.Context, string) error { return nil } +func (r *plannerRuntime) Reset(context.Context, string) error { return nil } +func (r *plannerRuntime) SendCtrlAltDel(context.Context, string) error { return nil } +func (r *plannerRuntime) Running(string) bool { return false } +func (r *plannerRuntime) VNCSocket(string) string { return "" } +func (r *plannerRuntime) Metrics(string) (qemu.Metrics, error) { return qemu.Metrics{}, nil } + +type legacyPlannerRuntime struct{ started int } + +func (r *legacyPlannerRuntime) Start(context.Context, store.VM) error { r.started++; return nil } +func (r *legacyPlannerRuntime) Stop(context.Context, string) error { return nil } +func (r *legacyPlannerRuntime) Reset(context.Context, string) error { return nil } +func (r *legacyPlannerRuntime) SendCtrlAltDel(context.Context, string) error { return nil } +func (r *legacyPlannerRuntime) Running(string) bool { return false } +func (r *legacyPlannerRuntime) VNCSocket(string) string { return "" } +func (r *legacyPlannerRuntime) Metrics(string) (qemu.Metrics, error) { return qemu.Metrics{}, nil } + +func TestServerStartVMPlannerBranches(t *testing.T) { + ctx := context.Background() + server := &Server{startPlanner: &plannerStub{err: errors.New("plan failed")}, vms: &plannerRuntime{}} + if err := server.startVM(ctx, store.VM{}); err == nil { + t.Fatal("startVM accepted a planner error") + } + legacy := &legacyPlannerRuntime{} + server = &Server{startPlanner: &plannerStub{}, vms: legacy} + if err := server.startVM(ctx, store.VM{}); err != nil || legacy.started != 1 { + t.Fatalf("local start = %v, runtime = %+v", err, legacy) + } + runtime := &plannerRuntime{} + plan := qemu.StartPlan{Attachments: map[int64]blockdev.Attachment{1: {Protocol: blockdev.ProtocolISCSI}}} + server = &Server{startPlanner: &plannerStub{plan: plan}, vms: runtime} + if err := server.startVM(ctx, store.VM{}); err != nil || runtime.startedPlan != 1 { + t.Fatalf("prepared start = %v, runtime = %+v", err, runtime) + } + legacy = &legacyPlannerRuntime{} + server = &Server{startPlanner: &plannerStub{plan: plan}, vms: legacy} + if err := server.startVM(ctx, store.VM{}); err == nil { + t.Fatal("startVM accepted a legacy runtime with a remote plan") + } + server.ConfigureStartPlanner(&plannerStub{}) + if server.startPlanner == nil { + t.Fatal("ConfigureStartPlanner did not set the planner") + } +} diff --git a/internal/gateway/kouja_branches_internal_test.go b/internal/gateway/kouja_branches_internal_test.go new file mode 100644 index 0000000..200da5f --- /dev/null +++ b/internal/gateway/kouja_branches_internal_test.go @@ -0,0 +1,242 @@ +package gateway + +import ( + "context" + "errors" + "path/filepath" + "sync" + "testing" + + "github.com/rs/zerolog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + rookeryv1 "github.com/voidstarit/rookery/design/gen/rookery/v1" + "github.com/voidstarit/rookery/internal/blockdev" + "github.com/voidstarit/rookery/internal/qemu" + "github.com/voidstarit/rookery/internal/readiness" + "github.com/voidstarit/rookery/internal/storage" + "github.com/voidstarit/rookery/internal/store" + "github.com/voidstarit/rookery/internal/vmstart" +) + +type branchDriver struct { + createResult storage.CreatedDisk + createErr error + health storage.Health + healthErr error + prepared blockdev.Attachment + prepareErr error +} + +func (d *branchDriver) Kind() string { return store.DiskSourceKouja } +func (d *branchDriver) CreateDisk(context.Context, store.StorageBackend, storage.CreateDiskRequest) (storage.CreatedDisk, error) { + return d.createResult, d.createErr +} +func (d *branchDriver) DeleteDisk(context.Context, store.StorageBackend, store.Disk) error { + return nil +} +func (d *branchDriver) Health(context.Context, store.StorageBackend, *store.Disk) (storage.Health, error) { + return d.health, d.healthErr +} +func (d *branchDriver) PrepareStart(context.Context, store.StorageBackend, store.Disk) (blockdev.Attachment, error) { + return d.prepared, d.prepareErr +} + +func validCreatedDisk() storage.CreatedDisk { + return storage.CreatedDisk{ + Metadata: store.DiskBackendMetadata{ + Version: 1, Driver: store.DiskSourceKouja, + Volume: store.VolumeMetadata{VolumeID: "volume-1", ResourceID: "resource-1", Owner: "owner", StorageClass: "nvme"}, + Attachment: blockdev.Reference{Protocol: blockdev.ProtocolISCSI, ISCSI: &blockdev.ISCSIReference{ + Portal: "10.0.0.1:3260", Target: "iqn.x", LUN: 0, + }}, + }, + SizeBytes: 5 << 30, + } +} + +func branchStore(t *testing.T) (*store.Store, store.StorageBackend) { + t.Helper() + st, err := store.Open(filepath.Join(t.TempDir(), "gateway-branches.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + backend, err := st.CreateStorageBackend(context.Background(), store.StorageBackend{Name: "kouja", Driver: store.DiskSourceKouja}) + if err != nil { + t.Fatal(err) + } + return st, backend +} + +func branchDiskService(st *store.Store, registry *storage.Registry) *diskService { + return &diskService{store: st, registry: registry, mu: &sync.Mutex{}, logger: zerolog.Nop()} +} + +func TestCreateKoujaDiskBranches(t *testing.T) { + ctx := context.Background() + st, backend := branchStore(t) + size := int32(5) + request := &rookeryv1.CreateDiskRequest{Name: "branch", SizeGb: &size} + if _, err := branchDiskService(st, nil).createKoujaDisk(ctx, request, "branch", backend, 5); err == nil { + t.Fatal("createKoujaDisk accepted a nil registry") + } + if _, err := branchDiskService(st, storage.NewRegistry()).createKoujaDisk(ctx, request, "branch", backend, 5); err == nil { + t.Fatal("createKoujaDisk accepted a missing driver") + } + // Duplicate name and the out-of-range/default size paths. + existing, err := st.CreateDisk(ctx, store.Disk{Name: "existing", SizeGB: 5, VG: "vg0", ThinPool: "pool"}) + if err != nil { + t.Fatal(err) + } + driver := &branchDriver{createResult: validCreatedDisk()} + service := branchDiskService(st, storage.NewRegistry(driver)) + if _, err := service.createKoujaDisk(ctx, request, existing.Name, backend, 5); err == nil { + t.Fatal("createKoujaDisk accepted a duplicate name") + } + tooBig := int32(maxGatewayDiskGB + 1) + if _, err := service.CreateDisk(ctx, &rookeryv1.CreateDiskRequest{Name: "too-big", SizeGb: &tooBig, StorageBackendId: &backend.ID}); err == nil { + t.Fatal("CreateDisk accepted an oversized kouja disk") + } + zero := int32(0) + if _, err := service.CreateDisk(ctx, &rookeryv1.CreateDiskRequest{Name: "default-size", SizeGb: &zero, StorageBackendId: &backend.ID}); err != nil { + t.Fatalf("CreateDisk(default size) = %v", err) + } + // NVMe protocol selection plus a finalize failure (invalid metadata). + backend.Config.Kouja = &store.KoujaConfig{Protocol: "nvme-tcp", StorageClass: "nvme"} + badDriver := &branchDriver{createResult: storage.CreatedDisk{SizeBytes: 5 << 30, Metadata: store.DiskBackendMetadata{Attachment: blockdev.Reference{Protocol: blockdev.ProtocolISCSI}}}} + if _, err := branchDiskService(st, storage.NewRegistry(badDriver)).createKoujaDisk(ctx, &rookeryv1.CreateDiskRequest{Name: "bad-finalize", SizeGb: &size}, "bad-finalize", backend, 5); err == nil { + t.Fatal("createKoujaDisk accepted invalid finalize metadata") + } +} + +func TestDeleteKoujaDiskNilRegistry(t *testing.T) { + ctx := context.Background() + st, backend := branchStore(t) + disk, err := st.CreateDisk(ctx, store.Disk{Name: "remote", SizeGB: 5, StorageBackendID: &backend.ID, + Source: store.DiskSourceKouja, Protocol: string(blockdev.ProtocolISCSI), + BackendMetadata: store.DiskBackendMetadata{Volume: store.VolumeMetadata{VolumeID: "volume-1"}, + Attachment: blockdev.Reference{Protocol: blockdev.ProtocolISCSI, ISCSI: &blockdev.ISCSIReference{ + Portal: "10.0.0.1:3260", Target: "iqn.x", LUN: 0, + }}}}) + if err != nil { + t.Fatal(err) + } + if _, err := branchDiskService(st, nil).DeleteDisk(ctx, &rookeryv1.DeleteDiskRequest{Id: disk.ID}); err == nil { + t.Fatal("DeleteDisk accepted a nil registry") + } +} + +func TestNormalizeKoujaBackendBranches(t *testing.T) { + if _, err := normalizeStorageBackend(&rookeryv1.CreateStorageBackendRequest{Name: "x", Driver: "kouja", Config: &rookeryv1.StorageBackendConfig{}}); err == nil { + t.Fatal("normalizeStorageBackend accepted missing kouja config") + } + if _, err := normalizeStorageBackend(&rookeryv1.CreateStorageBackendRequest{Name: "x", Driver: "kouja", + Config: &rookeryv1.StorageBackendConfig{DriverConfig: &rookeryv1.StorageBackendConfig_Kouja{Kouja: &rookeryv1.KoujaConfig{Endpoint: "10.0.0.1:9443"}}}}); err == nil { + t.Fatal("normalizeStorageBackend accepted an invalid kouja config") + } +} + +type staticReadiness struct{ status readiness.Status } + +func (r staticReadiness) Status() readiness.Status { return r.status } + +func TestStorageBackendHealthBranches(t *testing.T) { + ctx := context.Background() + st, kouja := branchStore(t) + lvm, err := st.CreateStorageBackend(ctx, store.StorageBackend{Name: "lvm", Driver: "lvm-thin", Config: store.StorageBackendConfig{VG: "vg0", ThinPool: "pool"}}) + if err != nil { + t.Fatal(err) + } + unknown, err := st.CreateStorageBackend(ctx, store.StorageBackend{Name: "unknown", Driver: "unknown"}) + if err != nil { + t.Fatal(err) + } + service := &storageBackendService{store: st, readiness: staticReadiness{status: readiness.Status{ThinPool: readiness.ThinPoolStatus{ + ComponentStatus: readiness.ComponentStatus{Ready: true, Detail: "ready"}, + }}}, registry: storage.NewRegistry(&branchDriver{healthErr: errors.New("health failed")})} + if _, err := service.GetStorageBackendHealth(ctx, &rookeryv1.GetStorageBackendHealthRequest{Id: 999}); err == nil { + t.Fatal("health accepted a missing backend") + } + lvmHealth, err := service.GetStorageBackendHealth(ctx, &rookeryv1.GetStorageBackendHealthRequest{Id: lvm.ID}) + if err != nil || !lvmHealth.Ready { + t.Fatalf("lvm health = %+v, %v", lvmHealth, err) + } + unknownHealth, err := service.GetStorageBackendHealth(ctx, &rookeryv1.GetStorageBackendHealthRequest{Id: unknown.ID}) + if err != nil || unknownHealth.Detail == "" { + t.Fatalf("unknown health = %+v, %v", unknownHealth, err) + } + if _, err := service.GetStorageBackendHealth(ctx, &rookeryv1.GetStorageBackendHealthRequest{Id: kouja.ID}); err != nil { + t.Fatal(err) + } + // A backend with a disk exercises the sample lookup loop. + if _, err := st.CreateDisk(ctx, store.Disk{Name: "sample", SizeGB: 5, StorageBackendID: &kouja.ID, + Source: store.DiskSourceKouja, Protocol: string(blockdev.ProtocolISCSI), + BackendMetadata: store.DiskBackendMetadata{Volume: store.VolumeMetadata{VolumeID: "sample"}, + Attachment: blockdev.Reference{Protocol: blockdev.ProtocolISCSI, ISCSI: &blockdev.ISCSIReference{ + Portal: "10.0.0.1:3260", Target: "iqn.x", LUN: 0, + }}}}); err != nil { + t.Fatal(err) + } + if _, err := service.GetStorageBackendHealth(ctx, &rookeryv1.GetStorageBackendHealthRequest{Id: kouja.ID}); err != nil { + t.Fatal(err) + } + if _, err := service.TestStorageBackendDataPath(ctx, &rookeryv1.TestStorageBackendDataPathRequest{Id: kouja.ID}); status.Code(err) != codes.Unimplemented { + t.Fatalf("TestStorageBackendDataPath = %v", err) + } + if _, err := (&storageBackendService{}).withDeletionBacklog(ctx, &rookeryv1.StorageBackendHealth{}); err != nil { + t.Fatal(err) + } +} + +func TestWithStartPlannerOption(t *testing.T) { + st, _ := branchStore(t) + gateway, err := New(st, staticReadiness{}, WithStartPlanner(vmstart.NewPlanner(st, storage.NewRegistry()))) + if err != nil { + t.Fatal(err) + } + t.Cleanup(gateway.Close) +} + +func TestVMActionsPlannerBranches(t *testing.T) { + ctx := context.Background() + st, backend := branchStore(t) + disk, err := st.CreateDisk(ctx, store.Disk{Name: "remote", SizeGB: 5, StorageBackendID: &backend.ID, + Source: store.DiskSourceKouja, Protocol: string(blockdev.ProtocolISCSI), + BackendMetadata: store.DiskBackendMetadata{Volume: store.VolumeMetadata{VolumeID: "volume-1"}, + Attachment: blockdev.Reference{Protocol: blockdev.ProtocolISCSI, ISCSI: &blockdev.ISCSIReference{ + Portal: "10.0.0.1:3260", Target: "iqn.x", LUN: 0, + }}}}) + if err != nil { + t.Fatal(err) + } + vm, err := st.CreateVM(ctx, store.VM{Name: "vm", VCPUs: 1, MemoryMB: 512, DiskIDs: []int64{disk.ID}}) + if err != nil { + t.Fatal(err) + } + // Planner error. + planner := vmstart.NewPlanner(st, storage.NewRegistry()) + service := &vmService{store: st, planner: planner, runtime: &legacyRuntime{}} + if err := service.startVM(ctx, vm); err == nil { + t.Fatal("startVM accepted a planner error") + } + // Non-empty plan with a runtime that only implements the old Start method. + driver := &branchDriver{prepared: blockdev.Attachment{Protocol: blockdev.ProtocolISCSI, ISCSI: &blockdev.ISCSIAttachment{ + ISCSIReference: blockdev.ISCSIReference{Portal: "10.0.0.1:3260", Target: "iqn.x", LUN: 0}, + Username: "user", Credential: "secret", + }}} + service.planner = vmstart.NewPlanner(st, storage.NewRegistry(driver)) + if err := service.startVM(ctx, vm); err == nil { + t.Fatal("startVM accepted a runtime without PreparedStarter") + } +} + +type legacyRuntime struct{} + +func (r *legacyRuntime) Start(context.Context, store.VM) error { return nil } +func (r *legacyRuntime) Stop(context.Context, string) error { return nil } +func (r *legacyRuntime) Reset(context.Context, string) error { return nil } +func (r *legacyRuntime) SendCtrlAltDel(context.Context, string) error { return nil } +func (r *legacyRuntime) Running(string) bool { return false } +func (r *legacyRuntime) Metrics(string) (qemu.Metrics, error) { return qemu.Metrics{}, nil }