diff --git a/api/next.pb.txt b/api/next.pb.txt index e69de29..d674f01 100755 --- a/api/next.pb.txt +++ b/api/next.pb.txt @@ -0,0 +1,528 @@ +file { + name: "github.com/spin-stack/spinbox/api/services/bundle/v1/bundle.proto" + package: "containerd.vminitd.services.bundle.v1" + message_type { + name: "CreateRequest" + field { + name: "id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "id" + } + field { + name: "files" + number: 2 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".containerd.vminitd.services.bundle.v1.CreateRequest.FilesEntry" + json_name: "files" + } + nested_type { + name: "FilesEntry" + field { + name: "key" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "key" + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_BYTES + json_name: "value" + } + options { + map_entry: true + } + } + } + message_type { + name: "CreateResponse" + field { + name: "bundle" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "bundle" + } + } + service { + name: "Bundle" + method { + name: "Create" + input_type: ".containerd.vminitd.services.bundle.v1.CreateRequest" + output_type: ".containerd.vminitd.services.bundle.v1.CreateResponse" + } + } + options { + go_package: "github.com/spin-stack/spinbox/api/services/bundle/v1;bundle" + } + syntax: "proto3" +} +file { + name: "github.com/spin-stack/spinbox/api/services/stdio/v1/stdio.proto" + package: "containerd.vminitd.services.stdio.v1" + message_type { + name: "WriteStdinRequest" + field { + name: "container_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "containerId" + } + field { + name: "exec_id" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "execId" + } + field { + name: "data" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_BYTES + json_name: "data" + } + } + message_type { + name: "WriteStdinResponse" + field { + name: "bytes_written" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "bytesWritten" + } + } + message_type { + name: "ReadOutputRequest" + field { + name: "container_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "containerId" + } + field { + name: "exec_id" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "execId" + } + } + message_type { + name: "OutputChunk" + field { + name: "data" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_BYTES + json_name: "data" + } + field { + name: "eof" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_BOOL + json_name: "eof" + } + } + message_type { + name: "CloseStdinRequest" + field { + name: "container_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "containerId" + } + field { + name: "exec_id" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "execId" + } + } + message_type { + name: "CloseStdinResponse" + } + service { + name: "StdIO" + method { + name: "WriteStdin" + input_type: ".containerd.vminitd.services.stdio.v1.WriteStdinRequest" + output_type: ".containerd.vminitd.services.stdio.v1.WriteStdinResponse" + } + method { + name: "ReadStdout" + input_type: ".containerd.vminitd.services.stdio.v1.ReadOutputRequest" + output_type: ".containerd.vminitd.services.stdio.v1.OutputChunk" + server_streaming: true + } + method { + name: "ReadStderr" + input_type: ".containerd.vminitd.services.stdio.v1.ReadOutputRequest" + output_type: ".containerd.vminitd.services.stdio.v1.OutputChunk" + server_streaming: true + } + method { + name: "CloseStdin" + input_type: ".containerd.vminitd.services.stdio.v1.CloseStdinRequest" + output_type: ".containerd.vminitd.services.stdio.v1.CloseStdinResponse" + } + } + options { + go_package: "github.com/spin-stack/spinbox/api/services/stdio/v1;stdio" + } + syntax: "proto3" +} +file { + name: "google/protobuf/empty.proto" + package: "google.protobuf" + message_type { + name: "Empty" + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "EmptyProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/emptypb" + cc_enable_arenas: true + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "github.com/spin-stack/spinbox/api/services/system/v1/info.proto" + package: "containerd.vminitd.services.system.v1" + dependency: "google/protobuf/empty.proto" + message_type { + name: "InfoResponse" + field { + name: "version" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "version" + } + field { + name: "kernel_version" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "kernelVersion" + } + } + message_type { + name: "FreezeFilesystemsResponse" + field { + name: "frozen" + number: 1 + label: LABEL_REPEATED + type: TYPE_STRING + json_name: "frozen" + } + } + message_type { + name: "OfflineCPURequest" + field { + name: "cpu_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "cpuId" + } + } + message_type { + name: "OnlineCPURequest" + field { + name: "cpu_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "cpuId" + } + } + message_type { + name: "OfflineMemoryRequest" + field { + name: "memory_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "memoryId" + } + } + message_type { + name: "OnlineMemoryRequest" + field { + name: "memory_id" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "memoryId" + } + } + service { + name: "System" + method { + name: "Info" + input_type: ".google.protobuf.Empty" + output_type: ".containerd.vminitd.services.system.v1.InfoResponse" + } + method { + name: "PrepareShutdown" + input_type: ".google.protobuf.Empty" + output_type: ".google.protobuf.Empty" + } + method { + name: "FreezeFilesystems" + input_type: ".google.protobuf.Empty" + output_type: ".containerd.vminitd.services.system.v1.FreezeFilesystemsResponse" + } + method { + name: "ThawFilesystems" + input_type: ".google.protobuf.Empty" + output_type: ".google.protobuf.Empty" + } + method { + name: "OfflineCPU" + input_type: ".containerd.vminitd.services.system.v1.OfflineCPURequest" + output_type: ".google.protobuf.Empty" + } + method { + name: "OnlineCPU" + input_type: ".containerd.vminitd.services.system.v1.OnlineCPURequest" + output_type: ".google.protobuf.Empty" + } + method { + name: "OfflineMemory" + input_type: ".containerd.vminitd.services.system.v1.OfflineMemoryRequest" + output_type: ".google.protobuf.Empty" + } + method { + name: "OnlineMemory" + input_type: ".containerd.vminitd.services.system.v1.OnlineMemoryRequest" + output_type: ".google.protobuf.Empty" + } + } + options { + go_package: "github.com/spin-stack/spinbox/api/services/system/v1;system" + } + syntax: "proto3" +} +file { + name: "github.com/containerd/containerd/api/types/fieldpath.proto" + package: "containerd.types" + dependency: "google/protobuf/descriptor.proto" + extension { + name: "fieldpath_all" + extendee: ".google.protobuf.FileOptions" + number: 63300 + label: LABEL_OPTIONAL + type: TYPE_BOOL + json_name: "fieldpathAll" + proto3_optional: true + } + extension { + name: "fieldpath" + extendee: ".google.protobuf.MessageOptions" + number: 64400 + label: LABEL_OPTIONAL + type: TYPE_BOOL + json_name: "fieldpath" + proto3_optional: true + } + options { + go_package: "github.com/containerd/containerd/api/types;types" + } + syntax: "proto3" +} +file { + name: "google/protobuf/any.proto" + package: "google.protobuf" + message_type { + name: "Any" + field { + name: "type_url" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "typeUrl" + } + field { + name: "value" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_BYTES + json_name: "value" + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "AnyProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/anypb" + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "google/protobuf/timestamp.proto" + package: "google.protobuf" + message_type { + name: "Timestamp" + field { + name: "seconds" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_INT64 + json_name: "seconds" + } + field { + name: "nanos" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_INT32 + json_name: "nanos" + } + } + options { + java_package: "com.google.protobuf" + java_outer_classname: "TimestampProto" + java_multiple_files: true + go_package: "google.golang.org/protobuf/types/known/timestamppb" + cc_enable_arenas: true + objc_class_prefix: "GPB" + csharp_namespace: "Google.Protobuf.WellKnownTypes" + } + syntax: "proto3" +} +file { + name: "github.com/containerd/containerd/api/types/event.proto" + package: "containerd.types" + dependency: "github.com/containerd/containerd/api/types/fieldpath.proto" + dependency: "google/protobuf/any.proto" + dependency: "google/protobuf/timestamp.proto" + message_type { + name: "Envelope" + field { + name: "timestamp" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Timestamp" + json_name: "timestamp" + } + field { + name: "namespace" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "namespace" + } + field { + name: "topic" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "topic" + } + field { + name: "event" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Any" + json_name: "event" + } + options { + 64400: 1 + } + } + options { + go_package: "github.com/containerd/containerd/api/types;types" + } + syntax: "proto3" +} +file { + name: "github.com/spin-stack/spinbox/api/services/vmevents/v1/events.proto" + package: "spinbox.services.vmevents.v1" + dependency: "github.com/containerd/containerd/api/types/event.proto" + dependency: "google/protobuf/empty.proto" + service { + name: "Events" + method { + name: "Stream" + input_type: ".google.protobuf.Empty" + output_type: ".containerd.types.Envelope" + server_streaming: true + } + } + options { + go_package: "github.com/spin-stack/spinbox/api/services/vmevents/v1;vmevents" + } + syntax: "proto3" +} +file { + name: "github.com/spin-stack/spinbox/api/types/spinbox/v1/options.proto" + package: "io.containerd.spinbox.v1" + message_type { + name: "SpinboxOpts" + field { + name: "boot_cpus" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "bootCpus" + } + field { + name: "max_cpus" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "maxCpus" + } + field { + name: "memory_mb" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "memoryMb" + } + field { + name: "max_memory_mb" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "maxMemoryMb" + } + field { + name: "io_uid" + number: 7 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "ioUid" + } + field { + name: "io_gid" + number: 8 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "ioGid" + } + } + options { + go_package: "github.com/spin-stack/spinbox/api/types/spinbox/v1;spinbox" + } + syntax: "proto3" +} diff --git a/api/services/system/v1/info.pb.go b/api/services/system/v1/info.pb.go index 5a8dc66..891b916 100644 --- a/api/services/system/v1/info.pb.go +++ b/api/services/system/v1/info.pb.go @@ -76,6 +76,51 @@ func (x *InfoResponse) GetKernelVersion() string { return "" } +type FreezeFilesystemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // frozen lists the mount points that were frozen by this call. + Frozen []string `protobuf:"bytes,1,rep,name=frozen,proto3" json:"frozen,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FreezeFilesystemsResponse) Reset() { + *x = FreezeFilesystemsResponse{} + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FreezeFilesystemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FreezeFilesystemsResponse) ProtoMessage() {} + +func (x *FreezeFilesystemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[1] + 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 FreezeFilesystemsResponse.ProtoReflect.Descriptor instead. +func (*FreezeFilesystemsResponse) Descriptor() ([]byte, []int) { + return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{1} +} + +func (x *FreezeFilesystemsResponse) GetFrozen() []string { + if x != nil { + return x.Frozen + } + return nil +} + type OfflineCPURequest struct { state protoimpl.MessageState `protogen:"open.v1"` // cpu_id is the logical CPU ID to offline (zero-indexed, e.g., 0, 1, 2). @@ -87,7 +132,7 @@ type OfflineCPURequest struct { func (x *OfflineCPURequest) Reset() { *x = OfflineCPURequest{} - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[1] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -99,7 +144,7 @@ func (x *OfflineCPURequest) String() string { func (*OfflineCPURequest) ProtoMessage() {} func (x *OfflineCPURequest) ProtoReflect() protoreflect.Message { - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[1] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -112,7 +157,7 @@ func (x *OfflineCPURequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OfflineCPURequest.ProtoReflect.Descriptor instead. func (*OfflineCPURequest) Descriptor() ([]byte, []int) { - return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{1} + return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{2} } func (x *OfflineCPURequest) GetCpuID() uint32 { @@ -133,7 +178,7 @@ type OnlineCPURequest struct { func (x *OnlineCPURequest) Reset() { *x = OnlineCPURequest{} - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[2] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -145,7 +190,7 @@ func (x *OnlineCPURequest) String() string { func (*OnlineCPURequest) ProtoMessage() {} func (x *OnlineCPURequest) ProtoReflect() protoreflect.Message { - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[2] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -158,7 +203,7 @@ func (x *OnlineCPURequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OnlineCPURequest.ProtoReflect.Descriptor instead. func (*OnlineCPURequest) Descriptor() ([]byte, []int) { - return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{2} + return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{3} } func (x *OnlineCPURequest) GetCpuID() uint32 { @@ -179,7 +224,7 @@ type OfflineMemoryRequest struct { func (x *OfflineMemoryRequest) Reset() { *x = OfflineMemoryRequest{} - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[3] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -191,7 +236,7 @@ func (x *OfflineMemoryRequest) String() string { func (*OfflineMemoryRequest) ProtoMessage() {} func (x *OfflineMemoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[3] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -204,7 +249,7 @@ func (x *OfflineMemoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OfflineMemoryRequest.ProtoReflect.Descriptor instead. func (*OfflineMemoryRequest) Descriptor() ([]byte, []int) { - return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{3} + return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{4} } func (x *OfflineMemoryRequest) GetMemoryID() uint32 { @@ -225,7 +270,7 @@ type OnlineMemoryRequest struct { func (x *OnlineMemoryRequest) Reset() { *x = OnlineMemoryRequest{} - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[4] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -237,7 +282,7 @@ func (x *OnlineMemoryRequest) String() string { func (*OnlineMemoryRequest) ProtoMessage() {} func (x *OnlineMemoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[4] + mi := &file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -250,7 +295,7 @@ func (x *OnlineMemoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OnlineMemoryRequest.ProtoReflect.Descriptor instead. func (*OnlineMemoryRequest) Descriptor() ([]byte, []int) { - return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{4} + return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescGZIP(), []int{5} } func (x *OnlineMemoryRequest) GetMemoryID() uint32 { @@ -267,7 +312,9 @@ const file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDe "?github.com/spin-stack/spinbox/api/services/system/v1/info.proto\x12%containerd.vminitd.services.system.v1\x1a\x1bgoogle/protobuf/empty.proto\"O\n" + "\fInfoResponse\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12%\n" + - "\x0ekernel_version\x18\x02 \x01(\tR\rkernelVersion\"*\n" + + "\x0ekernel_version\x18\x02 \x01(\tR\rkernelVersion\"3\n" + + "\x19FreezeFilesystemsResponse\x12\x16\n" + + "\x06frozen\x18\x01 \x03(\tR\x06frozen\"*\n" + "\x11OfflineCPURequest\x12\x15\n" + "\x06cpu_id\x18\x01 \x01(\rR\x05cpuId\")\n" + "\x10OnlineCPURequest\x12\x15\n" + @@ -275,9 +322,12 @@ const file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDe "\x14OfflineMemoryRequest\x12\x1b\n" + "\tmemory_id\x18\x01 \x01(\rR\bmemoryId\"2\n" + "\x13OnlineMemoryRequest\x12\x1b\n" + - "\tmemory_id\x18\x01 \x01(\rR\bmemoryId2\xe5\x03\n" + + "\tmemory_id\x18\x01 \x01(\rR\bmemoryId2\xda\x05\n" + "\x06System\x12S\n" + - "\x04Info\x12\x16.google.protobuf.Empty\x1a3.containerd.vminitd.services.system.v1.InfoResponse\x12^\n" + + "\x04Info\x12\x16.google.protobuf.Empty\x1a3.containerd.vminitd.services.system.v1.InfoResponse\x12A\n" + + "\x0fPrepareShutdown\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12m\n" + + "\x11FreezeFilesystems\x12\x16.google.protobuf.Empty\x1a@.containerd.vminitd.services.system.v1.FreezeFilesystemsResponse\x12A\n" + + "\x0fThawFilesystems\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12^\n" + "\n" + "OfflineCPU\x128.containerd.vminitd.services.system.v1.OfflineCPURequest\x1a\x16.google.protobuf.Empty\x12\\\n" + "\tOnlineCPU\x127.containerd.vminitd.services.system.v1.OnlineCPURequest\x1a\x16.google.protobuf.Empty\x12d\n" + @@ -296,28 +346,35 @@ func file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDes return file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDescData } -var file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_goTypes = []any{ - (*InfoResponse)(nil), // 0: containerd.vminitd.services.system.v1.InfoResponse - (*OfflineCPURequest)(nil), // 1: containerd.vminitd.services.system.v1.OfflineCPURequest - (*OnlineCPURequest)(nil), // 2: containerd.vminitd.services.system.v1.OnlineCPURequest - (*OfflineMemoryRequest)(nil), // 3: containerd.vminitd.services.system.v1.OfflineMemoryRequest - (*OnlineMemoryRequest)(nil), // 4: containerd.vminitd.services.system.v1.OnlineMemoryRequest - (*emptypb.Empty)(nil), // 5: google.protobuf.Empty + (*InfoResponse)(nil), // 0: containerd.vminitd.services.system.v1.InfoResponse + (*FreezeFilesystemsResponse)(nil), // 1: containerd.vminitd.services.system.v1.FreezeFilesystemsResponse + (*OfflineCPURequest)(nil), // 2: containerd.vminitd.services.system.v1.OfflineCPURequest + (*OnlineCPURequest)(nil), // 3: containerd.vminitd.services.system.v1.OnlineCPURequest + (*OfflineMemoryRequest)(nil), // 4: containerd.vminitd.services.system.v1.OfflineMemoryRequest + (*OnlineMemoryRequest)(nil), // 5: containerd.vminitd.services.system.v1.OnlineMemoryRequest + (*emptypb.Empty)(nil), // 6: google.protobuf.Empty } var file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_depIdxs = []int32{ - 5, // 0: containerd.vminitd.services.system.v1.System.Info:input_type -> google.protobuf.Empty - 1, // 1: containerd.vminitd.services.system.v1.System.OfflineCPU:input_type -> containerd.vminitd.services.system.v1.OfflineCPURequest - 2, // 2: containerd.vminitd.services.system.v1.System.OnlineCPU:input_type -> containerd.vminitd.services.system.v1.OnlineCPURequest - 3, // 3: containerd.vminitd.services.system.v1.System.OfflineMemory:input_type -> containerd.vminitd.services.system.v1.OfflineMemoryRequest - 4, // 4: containerd.vminitd.services.system.v1.System.OnlineMemory:input_type -> containerd.vminitd.services.system.v1.OnlineMemoryRequest - 0, // 5: containerd.vminitd.services.system.v1.System.Info:output_type -> containerd.vminitd.services.system.v1.InfoResponse - 5, // 6: containerd.vminitd.services.system.v1.System.OfflineCPU:output_type -> google.protobuf.Empty - 5, // 7: containerd.vminitd.services.system.v1.System.OnlineCPU:output_type -> google.protobuf.Empty - 5, // 8: containerd.vminitd.services.system.v1.System.OfflineMemory:output_type -> google.protobuf.Empty - 5, // 9: containerd.vminitd.services.system.v1.System.OnlineMemory:output_type -> google.protobuf.Empty - 5, // [5:10] is the sub-list for method output_type - 0, // [0:5] is the sub-list for method input_type + 6, // 0: containerd.vminitd.services.system.v1.System.Info:input_type -> google.protobuf.Empty + 6, // 1: containerd.vminitd.services.system.v1.System.PrepareShutdown:input_type -> google.protobuf.Empty + 6, // 2: containerd.vminitd.services.system.v1.System.FreezeFilesystems:input_type -> google.protobuf.Empty + 6, // 3: containerd.vminitd.services.system.v1.System.ThawFilesystems:input_type -> google.protobuf.Empty + 2, // 4: containerd.vminitd.services.system.v1.System.OfflineCPU:input_type -> containerd.vminitd.services.system.v1.OfflineCPURequest + 3, // 5: containerd.vminitd.services.system.v1.System.OnlineCPU:input_type -> containerd.vminitd.services.system.v1.OnlineCPURequest + 4, // 6: containerd.vminitd.services.system.v1.System.OfflineMemory:input_type -> containerd.vminitd.services.system.v1.OfflineMemoryRequest + 5, // 7: containerd.vminitd.services.system.v1.System.OnlineMemory:input_type -> containerd.vminitd.services.system.v1.OnlineMemoryRequest + 0, // 8: containerd.vminitd.services.system.v1.System.Info:output_type -> containerd.vminitd.services.system.v1.InfoResponse + 6, // 9: containerd.vminitd.services.system.v1.System.PrepareShutdown:output_type -> google.protobuf.Empty + 1, // 10: containerd.vminitd.services.system.v1.System.FreezeFilesystems:output_type -> containerd.vminitd.services.system.v1.FreezeFilesystemsResponse + 6, // 11: containerd.vminitd.services.system.v1.System.ThawFilesystems:output_type -> google.protobuf.Empty + 6, // 12: containerd.vminitd.services.system.v1.System.OfflineCPU:output_type -> google.protobuf.Empty + 6, // 13: containerd.vminitd.services.system.v1.System.OnlineCPU:output_type -> google.protobuf.Empty + 6, // 14: containerd.vminitd.services.system.v1.System.OfflineMemory:output_type -> google.protobuf.Empty + 6, // 15: containerd.vminitd.services.system.v1.System.OnlineMemory:output_type -> google.protobuf.Empty + 8, // [8:16] is the sub-list for method output_type + 0, // [0:8] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -334,7 +391,7 @@ func file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_init() GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDesc), len(file_github_com_spin_stack_spinbox_api_services_system_v1_info_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/api/services/system/v1/info.proto b/api/services/system/v1/info.proto index 50aaf52..5fd527e 100644 --- a/api/services/system/v1/info.proto +++ b/api/services/system/v1/info.proto @@ -21,6 +21,21 @@ service System { // and commit flows can rely on a quiesced writable layer. rpc PrepareShutdown(google.protobuf.Empty) returns (google.protobuf.Empty); + // FreezeFilesystems freezes the container's writable filesystem(s) via the + // FIFREEZE ioctl. This flushes dirty page cache to the block device and + // quiesces the on-disk filesystem so the backing rwlayer image can be read + // consistently while the VM keeps running. The host calls this from Pause + // (before suspending vCPUs); each frozen mount must later be thawed. + // + // Returns: + // - INTERNAL: an FIFREEZE ioctl failed (already-frozen mounts are skipped) + rpc FreezeFilesystems(google.protobuf.Empty) returns (FreezeFilesystemsResponse); + + // ThawFilesystems thaws filesystems previously frozen by FreezeFilesystems. + // It is best-effort and idempotent: thawing a filesystem that is not frozen + // is not an error. The host calls this from Resume (after restarting vCPUs). + rpc ThawFilesystems(google.protobuf.Empty) returns (google.protobuf.Empty); + // OfflineCPU takes a CPU offline via sysfs before hot-unplug. // The CPU must have been previously onlined and cannot be CPU 0 (boot CPU). // @@ -66,6 +81,11 @@ message InfoResponse { string kernel_version = 2; } +message FreezeFilesystemsResponse { + // frozen lists the mount points that were frozen by this call. + repeated string frozen = 1; +} + message OfflineCPURequest { // cpu_id is the logical CPU ID to offline (zero-indexed, e.g., 0, 1, 2). // This corresponds to /sys/devices/system/cpu/cpu{N}. diff --git a/api/services/system/v1/info_ttrpc.pb.go b/api/services/system/v1/info_ttrpc.pb.go index e2f6cd6..4c7e655 100644 --- a/api/services/system/v1/info_ttrpc.pb.go +++ b/api/services/system/v1/info_ttrpc.pb.go @@ -11,6 +11,8 @@ import ( type TTRPCSystemService interface { Info(context.Context, *emptypb.Empty) (*InfoResponse, error) PrepareShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) + FreezeFilesystems(context.Context, *emptypb.Empty) (*FreezeFilesystemsResponse, error) + ThawFilesystems(context.Context, *emptypb.Empty) (*emptypb.Empty, error) OfflineCPU(context.Context, *OfflineCPURequest) (*emptypb.Empty, error) OnlineCPU(context.Context, *OnlineCPURequest) (*emptypb.Empty, error) OfflineMemory(context.Context, *OfflineMemoryRequest) (*emptypb.Empty, error) @@ -34,6 +36,20 @@ func RegisterTTRPCSystemService(srv *ttrpc.Server, svc TTRPCSystemService) { } return svc.PrepareShutdown(ctx, &req) }, + "FreezeFilesystems": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) { + var req emptypb.Empty + if err := unmarshal(&req); err != nil { + return nil, err + } + return svc.FreezeFilesystems(ctx, &req) + }, + "ThawFilesystems": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) { + var req emptypb.Empty + if err := unmarshal(&req); err != nil { + return nil, err + } + return svc.ThawFilesystems(ctx, &req) + }, "OfflineCPU": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) { var req OfflineCPURequest if err := unmarshal(&req); err != nil { @@ -92,6 +108,22 @@ func (c *ttrpcsystemClient) PrepareShutdown(ctx context.Context, req *emptypb.Em return &resp, nil } +func (c *ttrpcsystemClient) FreezeFilesystems(ctx context.Context, req *emptypb.Empty) (*FreezeFilesystemsResponse, error) { + var resp FreezeFilesystemsResponse + if err := c.client.Call(ctx, "containerd.vminitd.services.system.v1.System", "FreezeFilesystems", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +func (c *ttrpcsystemClient) ThawFilesystems(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + var resp emptypb.Empty + if err := c.client.Call(ctx, "containerd.vminitd.services.system.v1.System", "ThawFilesystems", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + func (c *ttrpcsystemClient) OfflineCPU(ctx context.Context, req *OfflineCPURequest) (*emptypb.Empty, error) { var resp emptypb.Empty if err := c.client.Call(ctx, "containerd.vminitd.services.system.v1.System", "OfflineCPU", req, &resp); err != nil { diff --git a/hack/install-proto-tools b/hack/install-proto-tools index 97bee08..f90568d 100755 --- a/hack/install-proto-tools +++ b/hack/install-proto-tools @@ -10,5 +10,5 @@ set -eu -o pipefail # install `protobuild` and other commands go install github.com/containerd/protobuild@v0.3.0 go install github.com/containerd/protobuild/cmd/go-fix-acronym@v0.3.0 -go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1 -go install github.com/containerd/ttrpc/cmd/protoc-gen-go-ttrpc@v1.2.5 +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 +go install github.com/containerd/ttrpc/cmd/protoc-gen-go-ttrpc@v1.2.7 diff --git a/internal/guest/services/system.go b/internal/guest/services/system.go index a4839a8..05815ba 100644 --- a/internal/guest/services/system.go +++ b/internal/guest/services/system.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "unsafe" @@ -41,7 +42,12 @@ const ( featuresFilePerms = 0600 ) -type systemService struct{} +type systemService struct { + // frozenMu guards frozen, the mount points frozen by FreezeFilesystems and + // awaiting ThawFilesystems. + frozenMu sync.Mutex + frozen []string +} var prepareShutdown = guestsystem.Cleanup @@ -224,6 +230,35 @@ func (s *systemService) PrepareShutdown(ctx context.Context, _ *emptypb.Empty) ( return &emptypb.Empty{}, nil } +// FreezeFilesystems freezes the container's writable filesystem(s) (FIFREEZE) +// so the backing rwlayer image can be read consistently while the VM runs. +func (s *systemService) FreezeFilesystems(ctx context.Context, _ *emptypb.Empty) (*api.FreezeFilesystemsResponse, error) { + frozen, err := guestsystem.FreezeWritableFilesystems(ctx) + if err != nil { + return nil, errgrpc.ToGRPC(err) + } + + s.frozenMu.Lock() + s.frozen = frozen + s.frozenMu.Unlock() + + return &api.FreezeFilesystemsResponse{Frozen: frozen}, nil +} + +// ThawFilesystems thaws filesystems previously frozen by FreezeFilesystems. +// It is idempotent: thawing when nothing is frozen is a no-op. +func (s *systemService) ThawFilesystems(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { + s.frozenMu.Lock() + paths := s.frozen + s.frozen = nil + s.frozenMu.Unlock() + + if err := guestsystem.ThawFilesystems(ctx, paths); err != nil { + return nil, errgrpc.ToGRPC(err) + } + return &emptypb.Empty{}, nil +} + func (s *systemService) OfflineCPU(ctx context.Context, req *api.OfflineCPURequest) (*emptypb.Empty, error) { cpuID := req.GetCpuID() if cpuID == 0 { diff --git a/internal/guest/vminit/system/freeze.go b/internal/guest/vminit/system/freeze.go new file mode 100644 index 0000000..90a0119 --- /dev/null +++ b/internal/guest/vminit/system/freeze.go @@ -0,0 +1,150 @@ +//go:build linux + +package system + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/containerd/log" + "golang.org/x/sys/unix" +) + +// FIFREEZE/FITHAW freeze and thaw a filesystem. They are not exported by +// x/sys/unix, so define them here. Both are _IOWR('X', 119|120, int); the int +// argument is ignored by the kernel. +const ( + fifreeze = 0xC0045877 + fithaw = 0xC0045878 +) + +// freezableFstypes are the writable filesystem types quiesced for a consistent +// snapshot. The container's writable layer (rwlayer) is ext4; read-only base +// layers are erofs and need no freeze. +var freezableFstypes = map[string]bool{"ext4": true} + +// FreezeWritableFilesystems freezes the container's writable filesystem(s) via +// FIFREEZE and returns the mount points that were frozen. This flushes dirty +// page cache to the block device and quiesces the on-disk filesystem so the +// backing image can be read consistently. On partial failure it thaws whatever +// it already froze and returns the error. +func FreezeWritableFilesystems(ctx context.Context) ([]string, error) { + mounts, err := writableMountPoints() + if err != nil { + return nil, err + } + + var frozen []string + for _, mp := range mounts { + if err := ioctlNoArg(mp, fifreeze); err != nil { + if thawErr := ThawFilesystems(ctx, frozen); thawErr != nil { + log.G(ctx).WithError(thawErr).Warn("rollback thaw failed after freeze error") + } + return nil, fmt.Errorf("FIFREEZE %s: %w", mp, err) + } + frozen = append(frozen, mp) + log.G(ctx).WithField("mount", mp).Debug("froze filesystem") + } + return frozen, nil +} + +// ThawFilesystems thaws the given mount points via FITHAW. It is best-effort and +// idempotent: it attempts every path and returns the first error, if any. +func ThawFilesystems(ctx context.Context, paths []string) error { + var firstErr error + for _, mp := range paths { + if err := ioctlNoArg(mp, fithaw); err != nil { + log.G(ctx).WithError(err).WithField("mount", mp).Warn("FITHAW failed") + if firstErr == nil { + firstErr = fmt.Errorf("FITHAW %s: %w", mp, err) + } + continue + } + log.G(ctx).WithField("mount", mp).Debug("thawed filesystem") + } + return firstErr +} + +// ioctlNoArg opens the mount point and issues an argument-less freeze/thaw ioctl. +func ioctlNoArg(mountPoint string, req uint) error { + f, err := os.Open(mountPoint) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + return unix.IoctlSetInt(int(f.Fd()), req, 0) +} + +// writableMountPoints returns mount points whose filesystem type is freezable, +// parsed from /proc/self/mountinfo. +func writableMountPoints() ([]string, error) { + data, err := os.ReadFile("/proc/self/mountinfo") + if err != nil { + return nil, err + } + return parseFreezableMounts(data), nil +} + +// parseFreezableMounts extracts freezable mount points from mountinfo content. +func parseFreezableMounts(data []byte) []string { + var mps []string + for _, line := range strings.Split(string(data), "\n") { + // mountinfo format (man 5 proc): + // id parent major:minor root mountpoint opts... - fstype source superopts + // The fstype follows the " - " separator field. + fields := strings.Fields(line) + if len(fields) < 5 { + continue + } + sep := -1 + for i, f := range fields { + if f == "-" { + sep = i + break + } + } + if sep < 0 || sep+1 >= len(fields) { + continue + } + if freezableFstypes[fields[sep+1]] { + mps = append(mps, unescapeMountField(fields[4])) + } + } + return mps +} + +// unescapeMountField decodes the octal escapes (\040 space, \011 tab, \012 +// newline, \134 backslash) that the kernel applies to mountinfo path fields. +func unescapeMountField(s string) string { + if !strings.Contains(s, `\`) { + return s + } + var b strings.Builder + for i := 0; i < len(s); i++ { + if s[i] == '\\' && i+3 < len(s) { + if v, ok := parseOctal3(s[i+1 : i+4]); ok { + b.WriteByte(v) + i += 3 + continue + } + } + b.WriteByte(s[i]) + } + return b.String() +} + +func parseOctal3(s string) (byte, bool) { + if len(s) != 3 { + return 0, false + } + var v int + for i := range 3 { + if s[i] < '0' || s[i] > '7' { + return 0, false + } + v = v*8 + int(s[i]-'0') + } + return byte(v), true +} diff --git a/internal/guest/vminit/system/freeze_test.go b/internal/guest/vminit/system/freeze_test.go new file mode 100644 index 0000000..2a4d128 --- /dev/null +++ b/internal/guest/vminit/system/freeze_test.go @@ -0,0 +1,42 @@ +//go:build linux + +package system + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseFreezableMounts(t *testing.T) { + // Realistic mountinfo: the rwlayer ext4 is freezable; erofs base layers, + // overlay rootfs and system mounts are not. + mountinfo := `21 20 0:19 / /proc rw,nosuid,nodev,noexec - proc procfs rw +22 20 0:20 / /sys rw,nosuid - sysfs sysfsdev rw +23 20 0:5 / /dev rw,nosuid - devtmpfs udev rw +30 20 254:0 / /run/spin-stack/rootfs/0 ro,relatime - erofs /dev/vda ro +31 20 254:16 / /run/spin-stack/rwlayer rw,relatime - ext4 /dev/vdb rw +32 20 0:21 / /run/spin-stack/rootfs ro - overlay overlayfs rw,lowerdir=x,upperdir=y +` + got := parseFreezableMounts([]byte(mountinfo)) + assert.Equal(t, []string{"/run/spin-stack/rwlayer"}, got) +} + +func TestParseFreezableMounts_EscapedPath(t *testing.T) { + // The kernel octal-escapes spaces in mountinfo path fields. + mountinfo := "40 20 254:16 / /run/spin\\040data rw - ext4 /dev/vdb rw\n" + got := parseFreezableMounts([]byte(mountinfo)) + assert.Equal(t, []string{"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/run/spin data"}, got) +} + +func TestParseFreezableMounts_None(t *testing.T) { + mountinfo := "30 20 254:0 / /rootfs ro - erofs /dev/vda ro\n" + assert.Empty(t, parseFreezableMounts([]byte(mountinfo))) +} + +func TestUnescapeMountField(t *testing.T) { + assert.Equal(t, "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/plain/path", unescapeMountField("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/plain/path")) + assert.Equal(t, "/a b", unescapeMountField("/a\\040b")) + assert.Equal(t, "/a\tb", unescapeMountField("/a\\011b")) + assert.Equal(t, "/a\\b", unescapeMountField("/a\\134b")) +} diff --git a/internal/shim/task/service.go b/internal/shim/task/service.go index 10c9767..6317355 100644 --- a/internal/shim/task/service.go +++ b/internal/shim/task/service.go @@ -95,6 +95,7 @@ import ( "github.com/containerd/ttrpc" "github.com/containerd/typeurl/v2" + systemapi "github.com/spin-stack/spinbox/api/services/system/v1" "github.com/spin-stack/spinbox/api/services/vmevents/v1" "github.com/spin-stack/spinbox/internal/host/network" "github.com/spin-stack/spinbox/internal/shim/cpuhotplug" @@ -1176,7 +1177,20 @@ func (s *service) Pause(ctx context.Context, r *taskAPI.PauseRequest) (*ptypes.E if err != nil { return nil, errgrpc.ToGRPC(fmt.Errorf("pause: %w", err)) } + + // Freeze the writable filesystem before suspending vCPUs (FIFREEZE must run + // in the live guest). This flushes the page cache and quiesces the on-disk + // rwlayer, so a paused container has a consistent backing image that a + // commit can read. FIFREEZE alone keeps the VM running; the QMP stop below + // then halts execution. + if err := s.freezeGuestFilesystems(ctx); err != nil { + return nil, errgrpc.ToGRPC(fmt.Errorf("pause: freeze guest filesystems: %w", err)) + } if err := vmi.Pause(ctx); err != nil { + // Roll back the freeze so the container is not left with a frozen fs. + if thawErr := s.thawGuestFilesystems(ctx); thawErr != nil { + log.G(ctx).WithError(thawErr).Warn("pause: thaw rollback failed after vm pause error") + } return nil, errgrpc.ToGRPC(fmt.Errorf("pause vm: %w", err)) } s.paused.Store(true) @@ -1204,10 +1218,45 @@ func (s *service) Resume(ctx context.Context, r *taskAPI.ResumeRequest) (*ptypes } s.paused.Store(false) + // Thaw after vCPUs are running again (FITHAW must run in the live guest). + if err := s.thawGuestFilesystems(ctx); err != nil { + return nil, errgrpc.ToGRPC(fmt.Errorf("resume: thaw guest filesystems: %w", err)) + } + s.send(&eventstypes.TaskResumed{ContainerID: r.ID}) return &ptypes.Empty{}, nil } +// freezeGuestFilesystems asks vminitd to FIFREEZE the container's writable +// filesystem(s) so the backing rwlayer image is consistent on disk. +func (s *service) freezeGuestFilesystems(ctx context.Context) error { + vmc, cleanup, err := s.getTaskClient(ctx) + if err != nil { + return err + } + defer cleanup() + + resp, err := systemapi.NewTTRPCSystemClient(vmc).FreezeFilesystems(ctx, &ptypes.Empty{}) + if err != nil { + return err + } + log.G(ctx).WithField("frozen", resp.GetFrozen()).Debug("froze guest filesystems") + return nil +} + +// thawGuestFilesystems asks vminitd to FITHAW filesystems frozen by a prior +// freeze. It is idempotent on the guest side. +func (s *service) thawGuestFilesystems(ctx context.Context) error { + vmc, cleanup, err := s.getTaskClient(ctx) + if err != nil { + return err + } + defer cleanup() + + _, err = systemapi.NewTTRPCSystemClient(vmc).ThawFilesystems(ctx, &ptypes.Empty{}) + return err +} + // Kill a process with the provided signal. func (s *service) Kill(ctx context.Context, r *taskAPI.KillRequest) (*ptypes.Empty, error) { done, err := s.beginRPC(false)