From 795fb3f916af4e9a4c48e30b51747c98ab83010e Mon Sep 17 00:00:00 2001 From: spsobole Date: Sat, 24 Jun 2023 13:57:08 -0600 Subject: [PATCH] Initial server for cloudinit images --- Makefile | 11 + README.md | 24 +- api/cloud/client.go | 38 + api/cloud/cloud.pb.go | 1815 +++++++++++++++++++++++++++ api/cloud/cloud.proto | 139 ++ api/cloud/cloud_grpc.pb.go | 368 ++++++ client/client.go | 74 -- client/inventory_vms.go | 284 ----- cmd/serviced/main.go | 12 + cmd/vmctl/command.go | 62 + cmd/vmctl/command_network.go | 44 + cmd/vmctl/command_vm.go | 255 ++++ cmd/vmctl/command_zone.go | 80 ++ cmd/vmctl/main.go | 91 +- configs/ubuntu-lunar.cloudinit.tmpl | 35 +- configs/ubuntu-lunar.esx.tmpl | 4 +- esx/client.go | 155 +++ esx/config.go | 9 + esx/defaults.go | 65 + esx/inventory_networks.go | 57 + esx/inventory_vms.go | 288 +++++ {client => esx}/vm.go | 97 +- go.mod | 17 +- go.sum | 32 + pkg/pprint/csv.go | 31 + pkg/pprint/human.go | 32 + pkg/pprint/printer.go | 44 + pkg/utils/json.go | 15 + server.json.example | 49 + service/config.go | 16 + service/service.go | 120 ++ service/service_vm.go | 223 ++++ 32 files changed, 4057 insertions(+), 529 deletions(-) create mode 100644 Makefile create mode 100644 api/cloud/client.go create mode 100644 api/cloud/cloud.pb.go create mode 100644 api/cloud/cloud.proto create mode 100644 api/cloud/cloud_grpc.pb.go delete mode 100644 client/client.go delete mode 100644 client/inventory_vms.go create mode 100644 cmd/serviced/main.go create mode 100644 cmd/vmctl/command.go create mode 100644 cmd/vmctl/command_network.go create mode 100644 cmd/vmctl/command_vm.go create mode 100644 cmd/vmctl/command_zone.go create mode 100644 esx/client.go create mode 100644 esx/config.go create mode 100644 esx/defaults.go create mode 100644 esx/inventory_networks.go create mode 100644 esx/inventory_vms.go rename {client => esx}/vm.go (50%) create mode 100644 pkg/pprint/csv.go create mode 100644 pkg/pprint/human.go create mode 100644 pkg/pprint/printer.go create mode 100644 pkg/utils/json.go create mode 100644 server.json.example create mode 100644 service/config.go create mode 100644 service/service.go create mode 100644 service/service_vm.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d90ffdb --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) +pwd := $(dir $(mkfile_path)) +base_dir := $(abspath $(shell git rev-parse --show-toplevel)) + +proto: + docker run --rm \ + -w /workdir \ + -v $(base_dir):/workdir \ + -it tools/protoc \ + protoc -I=/workdir/api/ --go_out=/workdir/api --go_opt=paths=source_relative --go-grpc_out=/workdir/api --go-grpc_opt=paths=source_relative /workdir/api/cloud/cloud.proto +.PHONY: proto diff --git a/README.md b/README.md index 1bc0105..f5dc86b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ # esxlib -Library/Service to interact with ESX server. \ No newline at end of file +Library/Service to interact with ESX server that allows us to use cloud init based images for quickly launching VMs + +# Where to get images: +* [Ubuntu Cloud Images\(https://cloud-images.ubuntu.com/) +* .... + +# Setup +See configs/ubuntu-lunar.cloudinit.tmpl, configs/ubuntu-lunar.esx.tmpl + +The .esxtmpl file is the VMware Vm definition template +The .cloudinit.tmpl is the cloudinit file template + +Change them to your needs, they are pretty basic and should work by default + +Then configure the server:\ +cp server.example.json to server.json +fill in your details and the slugs you want to present. Slugs are used to fill in the esx.tmpl file + +Make sure that ssh is enabled on your esx server. And that wherever you are running cmd/serviced can ssh to it. (I typically run serviced on the hypervisor itself) + +SSH is needed because without a commercial license we can't use the VIM API to create vms. + + diff --git a/api/cloud/client.go b/api/cloud/client.go new file mode 100644 index 0000000..d207518 --- /dev/null +++ b/api/cloud/client.go @@ -0,0 +1,38 @@ +package cloud + +import ( + "context" + "crypto/tls" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" +) + +type Client struct { + CloudClient +} + +func NewClient(apiAddress, apiToken string, insecure bool) (*Client, error) { + clientInterceptor := func(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return invoker(ctx, method, req, reply, cc, opts...) + } + + if apiToken != "" { + clientInterceptor = func(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", apiToken) + return invoker(ctx, method, req, reply, cc, opts...) + } + } + + // TODO: use tls verification.. but that means we would need to load our own certificates into the end device + creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: insecure}) + conn, err := grpc.Dial(apiAddress, grpc.WithTransportCredentials(creds), grpc.WithUnaryInterceptor(clientInterceptor)) + if err != nil { + return nil, err + } + + return &Client{ + CloudClient: NewCloudClient(conn), + }, nil +} diff --git a/api/cloud/cloud.pb.go b/api/cloud/cloud.pb.go new file mode 100644 index 0000000..da7f795 --- /dev/null +++ b/api/cloud/cloud.pb.go @@ -0,0 +1,1815 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.14.0 +// source: cloud/cloud.proto + +package cloud + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PowerState int32 + +const ( + PowerState_UNKNOWN PowerState = 0 + PowerState_OFF PowerState = 1 + PowerState_SUSPENDED PowerState = 2 + PowerState_ON PowerState = 3 +) + +// Enum value maps for PowerState. +var ( + PowerState_name = map[int32]string{ + 0: "UNKNOWN", + 1: "OFF", + 2: "SUSPENDED", + 3: "ON", + } + PowerState_value = map[string]int32{ + "UNKNOWN": 0, + "OFF": 1, + "SUSPENDED": 2, + "ON": 3, + } +) + +func (x PowerState) Enum() *PowerState { + p := new(PowerState) + *p = x + return p +} + +func (x PowerState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PowerState) Descriptor() protoreflect.EnumDescriptor { + return file_cloud_cloud_proto_enumTypes[0].Descriptor() +} + +func (PowerState) Type() protoreflect.EnumType { + return &file_cloud_cloud_proto_enumTypes[0] +} + +func (x PowerState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PowerState.Descriptor instead. +func (PowerState) EnumDescriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{0} +} + +type VMPowerRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + State PowerState `protobuf:"varint,2,opt,name=State,proto3,enum=api.cloud.PowerState" json:"State,omitempty"` +} + +func (x *VMPowerRequest) Reset() { + *x = VMPowerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMPowerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMPowerRequest) ProtoMessage() {} + +func (x *VMPowerRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMPowerRequest.ProtoReflect.Descriptor instead. +func (*VMPowerRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{0} +} + +func (x *VMPowerRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *VMPowerRequest) GetState() PowerState { + if x != nil { + return x.State + } + return PowerState_UNKNOWN +} + +type VMPowerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *VMPowerResponse) Reset() { + *x = VMPowerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMPowerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMPowerResponse) ProtoMessage() {} + +func (x *VMPowerResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMPowerResponse.ProtoReflect.Descriptor instead. +func (*VMPowerResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{1} +} + +type VMNetworkInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Lan string `protobuf:"bytes,2,opt,name=lan,proto3" json:"lan,omitempty"` +} + +func (x *VMNetworkInfo) Reset() { + *x = VMNetworkInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMNetworkInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMNetworkInfo) ProtoMessage() {} + +func (x *VMNetworkInfo) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMNetworkInfo.ProtoReflect.Descriptor instead. +func (*VMNetworkInfo) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{2} +} + +func (x *VMNetworkInfo) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *VMNetworkInfo) GetLan() string { + if x != nil { + return x.Lan + } + return "" +} + +type VMInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + State PowerState `protobuf:"varint,4,opt,name=State,proto3,enum=api.cloud.PowerState" json:"State,omitempty"` + Network []*VMNetworkInfo `protobuf:"bytes,5,rep,name=network,proto3" json:"network,omitempty"` +} + +func (x *VMInfo) Reset() { + *x = VMInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMInfo) ProtoMessage() {} + +func (x *VMInfo) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMInfo.ProtoReflect.Descriptor instead. +func (*VMInfo) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{3} +} + +func (x *VMInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *VMInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *VMInfo) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *VMInfo) GetState() PowerState { + if x != nil { + return x.State + } + return PowerState_UNKNOWN +} + +func (x *VMInfo) GetNetwork() []*VMNetworkInfo { + if x != nil { + return x.Network + } + return nil +} + +type VMListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ZoneId string `protobuf:"bytes,1,opt,name=zone_id,json=zoneId,proto3" json:"zone_id,omitempty"` +} + +func (x *VMListRequest) Reset() { + *x = VMListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMListRequest) ProtoMessage() {} + +func (x *VMListRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMListRequest.ProtoReflect.Descriptor instead. +func (*VMListRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{4} +} + +func (x *VMListRequest) GetZoneId() string { + if x != nil { + return x.ZoneId + } + return "" +} + +type VMListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Vms []*VMInfo `protobuf:"bytes,1,rep,name=vms,proto3" json:"vms,omitempty"` +} + +func (x *VMListResponse) Reset() { + *x = VMListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMListResponse) ProtoMessage() {} + +func (x *VMListResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMListResponse.ProtoReflect.Descriptor instead. +func (*VMListResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{5} +} + +func (x *VMListResponse) GetVms() []*VMInfo { + if x != nil { + return x.Vms + } + return nil +} + +type VMGetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // TODO: add some filters + // + // Types that are assignable to Search: + // + // *VMGetRequest_Id + // *VMGetRequest_Name + Search isVMGetRequest_Search `protobuf_oneof:"search"` +} + +func (x *VMGetRequest) Reset() { + *x = VMGetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMGetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMGetRequest) ProtoMessage() {} + +func (x *VMGetRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMGetRequest.ProtoReflect.Descriptor instead. +func (*VMGetRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{6} +} + +func (m *VMGetRequest) GetSearch() isVMGetRequest_Search { + if m != nil { + return m.Search + } + return nil +} + +func (x *VMGetRequest) GetId() string { + if x, ok := x.GetSearch().(*VMGetRequest_Id); ok { + return x.Id + } + return "" +} + +func (x *VMGetRequest) GetName() string { + if x, ok := x.GetSearch().(*VMGetRequest_Name); ok { + return x.Name + } + return "" +} + +type isVMGetRequest_Search interface { + isVMGetRequest_Search() +} + +type VMGetRequest_Id struct { + Id string `protobuf:"bytes,1,opt,name=id,proto3,oneof"` +} + +type VMGetRequest_Name struct { + Name string `protobuf:"bytes,2,opt,name=name,proto3,oneof"` +} + +func (*VMGetRequest_Id) isVMGetRequest_Search() {} + +func (*VMGetRequest_Name) isVMGetRequest_Search() {} + +type VMGetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Vm *VMInfo `protobuf:"bytes,1,opt,name=vm,proto3" json:"vm,omitempty"` +} + +func (x *VMGetResponse) Reset() { + *x = VMGetResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMGetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMGetResponse) ProtoMessage() {} + +func (x *VMGetResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMGetResponse.ProtoReflect.Descriptor instead. +func (*VMGetResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{7} +} + +func (x *VMGetResponse) GetVm() *VMInfo { + if x != nil { + return x.Vm + } + return nil +} + +type CreateOptions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CpuCount uint32 `protobuf:"varint,1,opt,name=cpu_count,json=cpuCount,proto3" json:"cpu_count,omitempty"` + MemoryMb uint32 `protobuf:"varint,2,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + DiskGb uint32 `protobuf:"varint,3,opt,name=disk_gb,json=diskGb,proto3" json:"disk_gb,omitempty"` + NetworkId string `protobuf:"bytes,4,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` +} + +func (x *CreateOptions) Reset() { + *x = CreateOptions{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateOptions) ProtoMessage() {} + +func (x *CreateOptions) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateOptions.ProtoReflect.Descriptor instead. +func (*CreateOptions) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{8} +} + +func (x *CreateOptions) GetCpuCount() uint32 { + if x != nil { + return x.CpuCount + } + return 0 +} + +func (x *CreateOptions) GetMemoryMb() uint32 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *CreateOptions) GetDiskGb() uint32 { + if x != nil { + return x.DiskGb + } + return 0 +} + +func (x *CreateOptions) GetNetworkId() string { + if x != nil { + return x.NetworkId + } + return "" +} + +type VMCreateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ZoneId string `protobuf:"bytes,1,opt,name=zone_id,json=zoneId,proto3" json:"zone_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + AuthorizedHosts []string `protobuf:"bytes,4,rep,name=AuthorizedHosts,proto3" json:"AuthorizedHosts,omitempty"` + InitScript string `protobuf:"bytes,5,opt,name=init_script,json=initScript,proto3" json:"init_script,omitempty"` +} + +func (x *VMCreateRequest) Reset() { + *x = VMCreateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMCreateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMCreateRequest) ProtoMessage() {} + +func (x *VMCreateRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMCreateRequest.ProtoReflect.Descriptor instead. +func (*VMCreateRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{9} +} + +func (x *VMCreateRequest) GetZoneId() string { + if x != nil { + return x.ZoneId + } + return "" +} + +func (x *VMCreateRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *VMCreateRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *VMCreateRequest) GetAuthorizedHosts() []string { + if x != nil { + return x.AuthorizedHosts + } + return nil +} + +func (x *VMCreateRequest) GetInitScript() string { + if x != nil { + return x.InitScript + } + return "" +} + +type VMCreateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Vm *VMInfo `protobuf:"bytes,1,opt,name=vm,proto3" json:"vm,omitempty"` +} + +func (x *VMCreateResponse) Reset() { + *x = VMCreateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMCreateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMCreateResponse) ProtoMessage() {} + +func (x *VMCreateResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMCreateResponse.ProtoReflect.Descriptor instead. +func (*VMCreateResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{10} +} + +func (x *VMCreateResponse) GetVm() *VMInfo { + if x != nil { + return x.Vm + } + return nil +} + +type VMDestroyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *VMDestroyRequest) Reset() { + *x = VMDestroyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMDestroyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMDestroyRequest) ProtoMessage() {} + +func (x *VMDestroyRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMDestroyRequest.ProtoReflect.Descriptor instead. +func (*VMDestroyRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{11} +} + +func (x *VMDestroyRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type VMDestroyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *VMDestroyResponse) Reset() { + *x = VMDestroyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMDestroyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMDestroyResponse) ProtoMessage() {} + +func (x *VMDestroyResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMDestroyResponse.ProtoReflect.Descriptor instead. +func (*VMDestroyResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{12} +} + +type Slug struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + CpuCount uint32 `protobuf:"varint,4,opt,name=cpu_count,json=cpuCount,proto3" json:"cpu_count,omitempty"` + MemoryMb uint32 `protobuf:"varint,5,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"` + DiskGb uint32 `protobuf:"varint,6,opt,name=disk_gb,json=diskGb,proto3" json:"disk_gb,omitempty"` + Cost uint32 `protobuf:"varint,7,opt,name=cost,proto3" json:"cost,omitempty"` +} + +func (x *Slug) Reset() { + *x = Slug{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Slug) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Slug) ProtoMessage() {} + +func (x *Slug) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Slug.ProtoReflect.Descriptor instead. +func (*Slug) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{13} +} + +func (x *Slug) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Slug) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Slug) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Slug) GetCpuCount() uint32 { + if x != nil { + return x.CpuCount + } + return 0 +} + +func (x *Slug) GetMemoryMb() uint32 { + if x != nil { + return x.MemoryMb + } + return 0 +} + +func (x *Slug) GetDiskGb() uint32 { + if x != nil { + return x.DiskGb + } + return 0 +} + +func (x *Slug) GetCost() uint32 { + if x != nil { + return x.Cost + } + return 0 +} + +type VMListSlugsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ZoneId string `protobuf:"bytes,1,opt,name=zone_id,json=zoneId,proto3" json:"zone_id,omitempty"` +} + +func (x *VMListSlugsRequest) Reset() { + *x = VMListSlugsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMListSlugsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMListSlugsRequest) ProtoMessage() {} + +func (x *VMListSlugsRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMListSlugsRequest.ProtoReflect.Descriptor instead. +func (*VMListSlugsRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{14} +} + +func (x *VMListSlugsRequest) GetZoneId() string { + if x != nil { + return x.ZoneId + } + return "" +} + +type VMListSlugsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Slugs []*Slug `protobuf:"bytes,1,rep,name=slugs,proto3" json:"slugs,omitempty"` +} + +func (x *VMListSlugsResponse) Reset() { + *x = VMListSlugsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VMListSlugsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VMListSlugsResponse) ProtoMessage() {} + +func (x *VMListSlugsResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VMListSlugsResponse.ProtoReflect.Descriptor instead. +func (*VMListSlugsResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{15} +} + +func (x *VMListSlugsResponse) GetSlugs() []*Slug { + if x != nil { + return x.Slugs + } + return nil +} + +type Zone struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` +} + +func (x *Zone) Reset() { + *x = Zone{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Zone) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Zone) ProtoMessage() {} + +func (x *Zone) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Zone.ProtoReflect.Descriptor instead. +func (*Zone) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{16} +} + +func (x *Zone) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Zone) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Zone) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type ZonesListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ZonesListRequest) Reset() { + *x = ZonesListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ZonesListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ZonesListRequest) ProtoMessage() {} + +func (x *ZonesListRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ZonesListRequest.ProtoReflect.Descriptor instead. +func (*ZonesListRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{17} +} + +type ZonesListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Zones []*Zone `protobuf:"bytes,1,rep,name=zones,proto3" json:"zones,omitempty"` +} + +func (x *ZonesListResponse) Reset() { + *x = ZonesListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ZonesListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ZonesListResponse) ProtoMessage() {} + +func (x *ZonesListResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ZonesListResponse.ProtoReflect.Descriptor instead. +func (*ZonesListResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{18} +} + +func (x *ZonesListResponse) GetZones() []*Zone { + if x != nil { + return x.Zones + } + return nil +} + +type Network struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + VLAN int32 `protobuf:"varint,3,opt,name=VLAN,proto3" json:"VLAN,omitempty"` +} + +func (x *Network) Reset() { + *x = Network{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Network) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Network) ProtoMessage() {} + +func (x *Network) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Network.ProtoReflect.Descriptor instead. +func (*Network) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{19} +} + +func (x *Network) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Network) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Network) GetVLAN() int32 { + if x != nil { + return x.VLAN + } + return 0 +} + +type NetworksListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ZoneId string `protobuf:"bytes,1,opt,name=zone_id,json=zoneId,proto3" json:"zone_id,omitempty"` +} + +func (x *NetworksListRequest) Reset() { + *x = NetworksListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NetworksListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworksListRequest) ProtoMessage() {} + +func (x *NetworksListRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworksListRequest.ProtoReflect.Descriptor instead. +func (*NetworksListRequest) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{20} +} + +func (x *NetworksListRequest) GetZoneId() string { + if x != nil { + return x.ZoneId + } + return "" +} + +type NetworksListResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Networks []*Network `protobuf:"bytes,1,rep,name=networks,proto3" json:"networks,omitempty"` +} + +func (x *NetworksListResponse) Reset() { + *x = NetworksListResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_cloud_cloud_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NetworksListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworksListResponse) ProtoMessage() {} + +func (x *NetworksListResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_cloud_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworksListResponse.ProtoReflect.Descriptor instead. +func (*NetworksListResponse) Descriptor() ([]byte, []int) { + return file_cloud_cloud_proto_rawDescGZIP(), []int{21} +} + +func (x *NetworksListResponse) GetNetworks() []*Network { + if x != nil { + return x.Networks + } + return nil +} + +var File_cloud_cloud_proto protoreflect.FileDescriptor + +var file_cloud_cloud_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x22, 0x4d, + 0x0a, 0x0e, 0x56, 0x4d, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x2b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x50, 0x6f, 0x77, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x11, 0x0a, + 0x0f, 0x56, 0x4d, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x3b, 0x0a, 0x0d, 0x56, 0x4d, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6c, + 0x61, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6c, 0x61, 0x6e, 0x22, 0xa1, 0x01, + 0x0a, 0x06, 0x56, 0x4d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x73, 0x6c, 0x75, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, + 0x12, 0x2b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x50, 0x6f, 0x77, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, + 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x22, 0x28, 0x0a, 0x0d, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x7a, 0x6f, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x35, 0x0a, 0x0e, 0x56, + 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, + 0x03, 0x76, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x03, 0x76, + 0x6d, 0x73, 0x22, 0x40, 0x0a, 0x0c, 0x56, 0x4d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x73, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x22, 0x32, 0x0a, 0x0d, 0x56, 0x4d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x02, 0x76, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x02, 0x76, 0x6d, 0x22, 0x81, 0x01, 0x0a, 0x0d, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x70, + 0x75, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x63, + 0x70, 0x75, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x65, 0x6d, 0x6f, 0x72, + 0x79, 0x5f, 0x6d, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x4d, 0x62, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x67, 0x62, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x64, 0x69, 0x73, 0x6b, 0x47, 0x62, 0x12, 0x1d, 0x0a, + 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x64, 0x22, 0x9d, 0x01, 0x0a, + 0x0f, 0x56, 0x4d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x7a, 0x6f, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, + 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x48, + 0x6f, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x6e, 0x69, 0x74, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x69, 0x6e, 0x69, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x22, 0x35, 0x0a, 0x10, + 0x56, 0x4d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x21, 0x0a, 0x02, 0x76, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x02, 0x76, 0x6d, 0x22, 0x22, 0x0a, 0x10, 0x56, 0x4d, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x13, 0x0a, 0x11, 0x56, 0x4d, 0x44, 0x65, 0x73, + 0x74, 0x72, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb3, 0x01, 0x0a, + 0x04, 0x53, 0x6c, 0x75, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x63, + 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, + 0x63, 0x70, 0x75, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x5f, 0x6d, 0x62, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x65, 0x6d, + 0x6f, 0x72, 0x79, 0x4d, 0x62, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x67, 0x62, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x64, 0x69, 0x73, 0x6b, 0x47, 0x62, 0x12, 0x12, + 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, + 0x73, 0x74, 0x22, 0x2d, 0x0a, 0x12, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6c, 0x75, 0x67, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x7a, 0x6f, 0x6e, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x7a, 0x6f, 0x6e, 0x65, 0x49, + 0x64, 0x22, 0x3c, 0x0a, 0x13, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6c, 0x75, 0x67, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x05, 0x73, 0x6c, 0x75, 0x67, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x2e, 0x53, 0x6c, 0x75, 0x67, 0x52, 0x05, 0x73, 0x6c, 0x75, 0x67, 0x73, 0x22, + 0x4c, 0x0a, 0x04, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x12, 0x0a, + 0x10, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x3a, 0x0a, 0x11, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x2e, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x41, 0x0a, + 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x56, 0x4c, 0x41, 0x4e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x56, 0x4c, 0x41, 0x4e, + 0x22, 0x2e, 0x0a, 0x13, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x7a, 0x6f, 0x6e, 0x65, 0x49, 0x64, + 0x22, 0x46, 0x0a, 0x14, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x08, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x2a, 0x39, 0x0a, 0x0a, 0x50, 0x6f, 0x77, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x46, 0x46, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, + 0x53, 0x55, 0x53, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x44, 0x10, 0x02, 0x12, 0x06, 0x0a, 0x02, 0x4f, + 0x4e, 0x10, 0x03, 0x32, 0xb8, 0x04, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x12, 0x3d, 0x0a, + 0x06, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x05, + 0x56, 0x4d, 0x47, 0x65, 0x74, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x2e, 0x56, 0x4d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x07, 0x56, 0x4d, 0x50, 0x6f, + 0x77, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, + 0x56, 0x4d, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x50, 0x6f, 0x77, + 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x56, 0x4d, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, + 0x4d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x46, 0x0a, 0x09, 0x56, 0x4d, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x12, 0x1b, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x44, 0x65, 0x73, 0x74, 0x72, + 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x56, 0x4d, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x6c, 0x75, 0x67, 0x73, 0x12, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x2e, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6c, 0x75, 0x67, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x2e, 0x56, 0x4d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6c, 0x75, 0x67, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x4c, 0x69, + 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x5a, + 0x6f, 0x6e, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x5a, 0x6f, 0x6e, 0x65, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, + 0x0c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x12, + 0x5a, 0x10, 0x65, 0x73, 0x78, 0x6c, 0x69, 0x62, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_cloud_cloud_proto_rawDescOnce sync.Once + file_cloud_cloud_proto_rawDescData = file_cloud_cloud_proto_rawDesc +) + +func file_cloud_cloud_proto_rawDescGZIP() []byte { + file_cloud_cloud_proto_rawDescOnce.Do(func() { + file_cloud_cloud_proto_rawDescData = protoimpl.X.CompressGZIP(file_cloud_cloud_proto_rawDescData) + }) + return file_cloud_cloud_proto_rawDescData +} + +var file_cloud_cloud_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_cloud_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_cloud_cloud_proto_goTypes = []interface{}{ + (PowerState)(0), // 0: api.cloud.PowerState + (*VMPowerRequest)(nil), // 1: api.cloud.VMPowerRequest + (*VMPowerResponse)(nil), // 2: api.cloud.VMPowerResponse + (*VMNetworkInfo)(nil), // 3: api.cloud.VMNetworkInfo + (*VMInfo)(nil), // 4: api.cloud.VMInfo + (*VMListRequest)(nil), // 5: api.cloud.VMListRequest + (*VMListResponse)(nil), // 6: api.cloud.VMListResponse + (*VMGetRequest)(nil), // 7: api.cloud.VMGetRequest + (*VMGetResponse)(nil), // 8: api.cloud.VMGetResponse + (*CreateOptions)(nil), // 9: api.cloud.CreateOptions + (*VMCreateRequest)(nil), // 10: api.cloud.VMCreateRequest + (*VMCreateResponse)(nil), // 11: api.cloud.VMCreateResponse + (*VMDestroyRequest)(nil), // 12: api.cloud.VMDestroyRequest + (*VMDestroyResponse)(nil), // 13: api.cloud.VMDestroyResponse + (*Slug)(nil), // 14: api.cloud.Slug + (*VMListSlugsRequest)(nil), // 15: api.cloud.VMListSlugsRequest + (*VMListSlugsResponse)(nil), // 16: api.cloud.VMListSlugsResponse + (*Zone)(nil), // 17: api.cloud.Zone + (*ZonesListRequest)(nil), // 18: api.cloud.ZonesListRequest + (*ZonesListResponse)(nil), // 19: api.cloud.ZonesListResponse + (*Network)(nil), // 20: api.cloud.Network + (*NetworksListRequest)(nil), // 21: api.cloud.NetworksListRequest + (*NetworksListResponse)(nil), // 22: api.cloud.NetworksListResponse +} +var file_cloud_cloud_proto_depIdxs = []int32{ + 0, // 0: api.cloud.VMPowerRequest.State:type_name -> api.cloud.PowerState + 0, // 1: api.cloud.VMInfo.State:type_name -> api.cloud.PowerState + 3, // 2: api.cloud.VMInfo.network:type_name -> api.cloud.VMNetworkInfo + 4, // 3: api.cloud.VMListResponse.vms:type_name -> api.cloud.VMInfo + 4, // 4: api.cloud.VMGetResponse.vm:type_name -> api.cloud.VMInfo + 4, // 5: api.cloud.VMCreateResponse.vm:type_name -> api.cloud.VMInfo + 14, // 6: api.cloud.VMListSlugsResponse.slugs:type_name -> api.cloud.Slug + 17, // 7: api.cloud.ZonesListResponse.zones:type_name -> api.cloud.Zone + 20, // 8: api.cloud.NetworksListResponse.networks:type_name -> api.cloud.Network + 5, // 9: api.cloud.Cloud.VMList:input_type -> api.cloud.VMListRequest + 7, // 10: api.cloud.Cloud.VMGet:input_type -> api.cloud.VMGetRequest + 1, // 11: api.cloud.Cloud.VMPower:input_type -> api.cloud.VMPowerRequest + 10, // 12: api.cloud.Cloud.VMCreate:input_type -> api.cloud.VMCreateRequest + 12, // 13: api.cloud.Cloud.VMDestroy:input_type -> api.cloud.VMDestroyRequest + 15, // 14: api.cloud.Cloud.VMListSlugs:input_type -> api.cloud.VMListSlugsRequest + 18, // 15: api.cloud.Cloud.ZonesList:input_type -> api.cloud.ZonesListRequest + 21, // 16: api.cloud.Cloud.NetworksList:input_type -> api.cloud.NetworksListRequest + 6, // 17: api.cloud.Cloud.VMList:output_type -> api.cloud.VMListResponse + 8, // 18: api.cloud.Cloud.VMGet:output_type -> api.cloud.VMGetResponse + 2, // 19: api.cloud.Cloud.VMPower:output_type -> api.cloud.VMPowerResponse + 11, // 20: api.cloud.Cloud.VMCreate:output_type -> api.cloud.VMCreateResponse + 13, // 21: api.cloud.Cloud.VMDestroy:output_type -> api.cloud.VMDestroyResponse + 16, // 22: api.cloud.Cloud.VMListSlugs:output_type -> api.cloud.VMListSlugsResponse + 19, // 23: api.cloud.Cloud.ZonesList:output_type -> api.cloud.ZonesListResponse + 22, // 24: api.cloud.Cloud.NetworksList:output_type -> api.cloud.NetworksListResponse + 17, // [17:25] is the sub-list for method output_type + 9, // [9:17] 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 +} + +func init() { file_cloud_cloud_proto_init() } +func file_cloud_cloud_proto_init() { + if File_cloud_cloud_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_cloud_cloud_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMPowerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMPowerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMNetworkInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMGetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMGetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateOptions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMCreateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMCreateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMDestroyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMDestroyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Slug); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMListSlugsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VMListSlugsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Zone); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ZonesListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ZonesListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Network); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NetworksListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_cloud_cloud_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NetworksListResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_cloud_cloud_proto_msgTypes[6].OneofWrappers = []interface{}{ + (*VMGetRequest_Id)(nil), + (*VMGetRequest_Name)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_cloud_cloud_proto_rawDesc, + NumEnums: 1, + NumMessages: 22, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_cloud_cloud_proto_goTypes, + DependencyIndexes: file_cloud_cloud_proto_depIdxs, + EnumInfos: file_cloud_cloud_proto_enumTypes, + MessageInfos: file_cloud_cloud_proto_msgTypes, + }.Build() + File_cloud_cloud_proto = out.File + file_cloud_cloud_proto_rawDesc = nil + file_cloud_cloud_proto_goTypes = nil + file_cloud_cloud_proto_depIdxs = nil +} diff --git a/api/cloud/cloud.proto b/api/cloud/cloud.proto new file mode 100644 index 0000000..8ffd2a6 --- /dev/null +++ b/api/cloud/cloud.proto @@ -0,0 +1,139 @@ +syntax = "proto3"; + +option go_package = "esxlib/api/cloud"; +package api.cloud; + + +service Cloud { + rpc VMList(VMListRequest) returns (VMListResponse); + rpc VMGet(VMGetRequest) returns (VMGetResponse); + rpc VMPower(VMPowerRequest) returns (VMPowerResponse); + + rpc VMCreate(VMCreateRequest) returns (VMCreateResponse); + rpc VMDestroy(VMDestroyRequest) returns (VMDestroyResponse); + + rpc VMListSlugs(VMListSlugsRequest) returns (VMListSlugsResponse); + + rpc ZonesList(ZonesListRequest) returns (ZonesListResponse); + rpc NetworksList(NetworksListRequest) returns (NetworksListResponse); +} + + +enum PowerState { + UNKNOWN = 0; + OFF = 1; + SUSPENDED = 2; + ON = 3; +} + +message VMPowerRequest { + string id = 1; + PowerState State = 2; +} + +message VMPowerResponse { +} + +message VMNetworkInfo{ + string address = 1; + string lan = 2; +} + +message VMInfo { + string id = 1; + string name = 2; + string slug = 3; + PowerState State = 4; + repeated VMNetworkInfo network = 5; +} + +message VMListRequest { + string zone_id = 1; +} + +message VMListResponse { + repeated VMInfo vms = 1; +} + +message VMGetRequest { + // TODO: add some filters + oneof search { + string id = 1; + string name = 2; + } +} + +message VMGetResponse { + VMInfo vm = 1; +} + +message CreateOptions { + uint32 cpu_count = 1; + uint32 memory_mb = 2; + uint32 disk_gb = 3; + string network_id = 4; +} + +message VMCreateRequest{ + string zone_id = 1; + string name = 2; + string slug = 3; + repeated string AuthorizedHosts = 4; + string init_script = 5; +} + +message VMCreateResponse{ + VMInfo vm = 1; +} + +message VMDestroyRequest{ + string id = 1; +} + +message VMDestroyResponse{ +} + +message Slug{ + string id = 1; + string name = 2; + string description = 3; + uint32 cpu_count = 4; + uint32 memory_mb = 5; + uint32 disk_gb = 6; + uint32 cost = 7; +} + +message VMListSlugsRequest{ + string zone_id = 1; +} + +message VMListSlugsResponse{ + repeated Slug slugs = 1; +} + +message Zone { + string id = 1; + string name = 2; + string description = 3; +} + +message ZonesListRequest{ +} + +message ZonesListResponse { + repeated Zone zones = 1; +} + +message Network { + string id = 1; + string name = 2; + int32 VLAN = 3; +} + +message NetworksListRequest { + string zone_id = 1; +} + +message NetworksListResponse { + repeated Network networks = 1; +} \ No newline at end of file diff --git a/api/cloud/cloud_grpc.pb.go b/api/cloud/cloud_grpc.pb.go new file mode 100644 index 0000000..21741aa --- /dev/null +++ b/api/cloud/cloud_grpc.pb.go @@ -0,0 +1,368 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.14.0 +// source: cloud/cloud.proto + +package cloud + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Cloud_VMList_FullMethodName = "/api.cloud.Cloud/VMList" + Cloud_VMGet_FullMethodName = "/api.cloud.Cloud/VMGet" + Cloud_VMPower_FullMethodName = "/api.cloud.Cloud/VMPower" + Cloud_VMCreate_FullMethodName = "/api.cloud.Cloud/VMCreate" + Cloud_VMDestroy_FullMethodName = "/api.cloud.Cloud/VMDestroy" + Cloud_VMListSlugs_FullMethodName = "/api.cloud.Cloud/VMListSlugs" + Cloud_ZonesList_FullMethodName = "/api.cloud.Cloud/ZonesList" + Cloud_NetworksList_FullMethodName = "/api.cloud.Cloud/NetworksList" +) + +// CloudClient is the client API for Cloud service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CloudClient interface { + VMList(ctx context.Context, in *VMListRequest, opts ...grpc.CallOption) (*VMListResponse, error) + VMGet(ctx context.Context, in *VMGetRequest, opts ...grpc.CallOption) (*VMGetResponse, error) + VMPower(ctx context.Context, in *VMPowerRequest, opts ...grpc.CallOption) (*VMPowerResponse, error) + VMCreate(ctx context.Context, in *VMCreateRequest, opts ...grpc.CallOption) (*VMCreateResponse, error) + VMDestroy(ctx context.Context, in *VMDestroyRequest, opts ...grpc.CallOption) (*VMDestroyResponse, error) + VMListSlugs(ctx context.Context, in *VMListSlugsRequest, opts ...grpc.CallOption) (*VMListSlugsResponse, error) + ZonesList(ctx context.Context, in *ZonesListRequest, opts ...grpc.CallOption) (*ZonesListResponse, error) + NetworksList(ctx context.Context, in *NetworksListRequest, opts ...grpc.CallOption) (*NetworksListResponse, error) +} + +type cloudClient struct { + cc grpc.ClientConnInterface +} + +func NewCloudClient(cc grpc.ClientConnInterface) CloudClient { + return &cloudClient{cc} +} + +func (c *cloudClient) VMList(ctx context.Context, in *VMListRequest, opts ...grpc.CallOption) (*VMListResponse, error) { + out := new(VMListResponse) + err := c.cc.Invoke(ctx, Cloud_VMList_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) VMGet(ctx context.Context, in *VMGetRequest, opts ...grpc.CallOption) (*VMGetResponse, error) { + out := new(VMGetResponse) + err := c.cc.Invoke(ctx, Cloud_VMGet_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) VMPower(ctx context.Context, in *VMPowerRequest, opts ...grpc.CallOption) (*VMPowerResponse, error) { + out := new(VMPowerResponse) + err := c.cc.Invoke(ctx, Cloud_VMPower_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) VMCreate(ctx context.Context, in *VMCreateRequest, opts ...grpc.CallOption) (*VMCreateResponse, error) { + out := new(VMCreateResponse) + err := c.cc.Invoke(ctx, Cloud_VMCreate_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) VMDestroy(ctx context.Context, in *VMDestroyRequest, opts ...grpc.CallOption) (*VMDestroyResponse, error) { + out := new(VMDestroyResponse) + err := c.cc.Invoke(ctx, Cloud_VMDestroy_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) VMListSlugs(ctx context.Context, in *VMListSlugsRequest, opts ...grpc.CallOption) (*VMListSlugsResponse, error) { + out := new(VMListSlugsResponse) + err := c.cc.Invoke(ctx, Cloud_VMListSlugs_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) ZonesList(ctx context.Context, in *ZonesListRequest, opts ...grpc.CallOption) (*ZonesListResponse, error) { + out := new(ZonesListResponse) + err := c.cc.Invoke(ctx, Cloud_ZonesList_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *cloudClient) NetworksList(ctx context.Context, in *NetworksListRequest, opts ...grpc.CallOption) (*NetworksListResponse, error) { + out := new(NetworksListResponse) + err := c.cc.Invoke(ctx, Cloud_NetworksList_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CloudServer is the server API for Cloud service. +// All implementations must embed UnimplementedCloudServer +// for forward compatibility +type CloudServer interface { + VMList(context.Context, *VMListRequest) (*VMListResponse, error) + VMGet(context.Context, *VMGetRequest) (*VMGetResponse, error) + VMPower(context.Context, *VMPowerRequest) (*VMPowerResponse, error) + VMCreate(context.Context, *VMCreateRequest) (*VMCreateResponse, error) + VMDestroy(context.Context, *VMDestroyRequest) (*VMDestroyResponse, error) + VMListSlugs(context.Context, *VMListSlugsRequest) (*VMListSlugsResponse, error) + ZonesList(context.Context, *ZonesListRequest) (*ZonesListResponse, error) + NetworksList(context.Context, *NetworksListRequest) (*NetworksListResponse, error) + mustEmbedUnimplementedCloudServer() +} + +// UnimplementedCloudServer must be embedded to have forward compatible implementations. +type UnimplementedCloudServer struct { +} + +func (UnimplementedCloudServer) VMList(context.Context, *VMListRequest) (*VMListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VMList not implemented") +} +func (UnimplementedCloudServer) VMGet(context.Context, *VMGetRequest) (*VMGetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VMGet not implemented") +} +func (UnimplementedCloudServer) VMPower(context.Context, *VMPowerRequest) (*VMPowerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VMPower not implemented") +} +func (UnimplementedCloudServer) VMCreate(context.Context, *VMCreateRequest) (*VMCreateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VMCreate not implemented") +} +func (UnimplementedCloudServer) VMDestroy(context.Context, *VMDestroyRequest) (*VMDestroyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VMDestroy not implemented") +} +func (UnimplementedCloudServer) VMListSlugs(context.Context, *VMListSlugsRequest) (*VMListSlugsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VMListSlugs not implemented") +} +func (UnimplementedCloudServer) ZonesList(context.Context, *ZonesListRequest) (*ZonesListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ZonesList not implemented") +} +func (UnimplementedCloudServer) NetworksList(context.Context, *NetworksListRequest) (*NetworksListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method NetworksList not implemented") +} +func (UnimplementedCloudServer) mustEmbedUnimplementedCloudServer() {} + +// UnsafeCloudServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CloudServer will +// result in compilation errors. +type UnsafeCloudServer interface { + mustEmbedUnimplementedCloudServer() +} + +func RegisterCloudServer(s grpc.ServiceRegistrar, srv CloudServer) { + s.RegisterService(&Cloud_ServiceDesc, srv) +} + +func _Cloud_VMList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VMListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).VMList(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_VMList_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).VMList(ctx, req.(*VMListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_VMGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VMGetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).VMGet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_VMGet_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).VMGet(ctx, req.(*VMGetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_VMPower_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VMPowerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).VMPower(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_VMPower_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).VMPower(ctx, req.(*VMPowerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_VMCreate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VMCreateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).VMCreate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_VMCreate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).VMCreate(ctx, req.(*VMCreateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_VMDestroy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VMDestroyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).VMDestroy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_VMDestroy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).VMDestroy(ctx, req.(*VMDestroyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_VMListSlugs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VMListSlugsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).VMListSlugs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_VMListSlugs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).VMListSlugs(ctx, req.(*VMListSlugsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_ZonesList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ZonesListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).ZonesList(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_ZonesList_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).ZonesList(ctx, req.(*ZonesListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Cloud_NetworksList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworksListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudServer).NetworksList(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Cloud_NetworksList_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudServer).NetworksList(ctx, req.(*NetworksListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Cloud_ServiceDesc is the grpc.ServiceDesc for Cloud service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Cloud_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.cloud.Cloud", + HandlerType: (*CloudServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "VMList", + Handler: _Cloud_VMList_Handler, + }, + { + MethodName: "VMGet", + Handler: _Cloud_VMGet_Handler, + }, + { + MethodName: "VMPower", + Handler: _Cloud_VMPower_Handler, + }, + { + MethodName: "VMCreate", + Handler: _Cloud_VMCreate_Handler, + }, + { + MethodName: "VMDestroy", + Handler: _Cloud_VMDestroy_Handler, + }, + { + MethodName: "VMListSlugs", + Handler: _Cloud_VMListSlugs_Handler, + }, + { + MethodName: "ZonesList", + Handler: _Cloud_ZonesList_Handler, + }, + { + MethodName: "NetworksList", + Handler: _Cloud_NetworksList_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "cloud/cloud.proto", +} diff --git a/client/client.go b/client/client.go deleted file mode 100644 index 737d434..0000000 --- a/client/client.go +++ /dev/null @@ -1,74 +0,0 @@ -package client - -import ( - "context" - "regexp" - - "esxlib/pkg/sshutil" - - "github.com/vmware/govmomi/session/cache" - "github.com/vmware/govmomi/vim25" - "github.com/vmware/govmomi/vim25/soap" -) - -var diskRegexp = regexp.MustCompile("\\[(.*?)\\] (.*)") - -type Option func(c *Client) - -type Client struct { - sshAuth *sshutil.Auth - vim *vim25.Client - - VirtualMachines VirtualMachines -} - -type ClientRef interface { - VIM() *vim25.Client -} - -func (c *Client) VIM() *vim25.Client { - return c.vim -} - -func WithSSH(host, user, password string) Option { - return func(c *Client) { - c.sshAuth = &sshutil.Auth{ - Host: host, - User: user, - Password: password, - } - } -} - -func NewClient(ctx context.Context, url string, insecure bool, opts ...Option) (*Client, error) { - // Parse URL from string - u, err := soap.ParseURL(url) - if err != nil { - return nil, err - } - - // Share govc's session cache - s := &cache.Session{ - URL: u, - Insecure: insecure, - } - - c := new(vim25.Client) - err = s.Login(ctx, c, nil) - if err != nil { - return nil, err - } - - client := &Client{ - vim: c, - } - - for _, opt := range opts { - opt(client) - } - - client.VirtualMachines.ClientRef = client - client.VirtualMachines.sshAuth = client.sshAuth - - return client, nil -} diff --git a/client/inventory_vms.go b/client/inventory_vms.go deleted file mode 100644 index fb056de..0000000 --- a/client/inventory_vms.go +++ /dev/null @@ -1,284 +0,0 @@ -package client - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "text/template" - - "esxlib/pkg/sshutil" - - "github.com/vmware/govmomi/find" - "github.com/vmware/govmomi/view" - "github.com/vmware/govmomi/vim25/mo" - "github.com/vmware/govmomi/vim25/soap" -) - -type VirtualMachines struct { - ClientRef - sshAuth *sshutil.Auth -} - -func (vms *VirtualMachines) List(ctx context.Context) ([]*VirtualMachine, error) { - m := view.NewManager(vms.VIM()) - v, err := m.CreateContainerView(ctx, vms.VIM().ServiceContent.RootFolder, []string{"VirtualMachine"}, true) - if err != nil { - return nil, err - } - defer v.Destroy(ctx) - - // Retrieve summary property for all machines - // Reference: http://pubs.vmware.com/vsphere-60/topic/com.vmware.wssdk.apiref.doc/vim.VirtualMachine.html - var moList []mo.VirtualMachine - err = v.Retrieve(ctx, []string{"VirtualMachine"}, nil, &moList) - if err != nil { - return nil, err - } - - vmList := make([]*VirtualMachine, 0) - - for idx := range moList { - vmList = append(vmList, &VirtualMachine{ - c: vms.VIM(), - sshAuth: vms.sshAuth, - ref: moList[idx].Reference(), - mo: &moList[idx], - }) - } - - return vmList, nil -} - -func (vms *VirtualMachines) Get(ctx context.Context, name string) (*VirtualMachine, error) { - vmList, err := vms.List(ctx) - if err != nil { - return nil, err - } - - for idx, vm := range vmList { - if vm.Name() == name { - return vmList[idx], nil - } - } - - return nil, errors.New("vm does not exist") -} - -func (vms *VirtualMachines) Create(ctx context.Context, name string, memSize uint, diskSize uint, initScript []byte) (*VirtualMachine, error) { - // we need some into about this ESX server from someplace - // DestinationPath: /vmfs/volumes/datastore1 - // SourcePath - container := "datastore1" - path := fmt.Sprintf("%s/%s.vmx", name, name) - - initScriptEncoded := "" - if len(initScript) != 0 { - initScriptEncoded = base64.StdEncoding.EncodeToString(initScript) - } - - tmpl, err := template.New("ubuntu-lunar.cloudinit.tmpl").ParseFiles("configs/ubuntu-lunar.cloudinit.tmpl") - if err != nil { - panic(err) - } - - config := struct { - Name string - Hostname string - MemSizeMB uint - CloudInitBase64 string - InitScriptBase64 string - }{ - Name: name, - Hostname: name, - MemSizeMB: memSize, - InitScriptBase64: initScriptEncoded, - } - - var b bytes.Buffer - w := bufio.NewWriter(&b) - - err = tmpl.Execute(w, config) - if err != nil { - panic(err) - } - err = w.Flush() - if err != nil { - panic(err) - } - - cloudInitEncoded := base64.StdEncoding.EncodeToString(b.Bytes()) - config.CloudInitBase64 = cloudInitEncoded - - fmt.Printf("%s\n", string(b.Bytes())) - - // Now encode the vm config - tmpl, err = template.New("ubuntu-lunar.esx.tmpl").ParseFiles("configs/ubuntu-lunar.esx.tmpl") - if err != nil { - panic(err) - } - - var upload bytes.Buffer - w = bufio.NewWriter(&upload) - - err = tmpl.Execute(w, config) - if err != nil { - panic(err) - } - err = w.Flush() - if err != nil { - panic(err) - } - - p := soap.DefaultUpload - - err = vms.Mkdir(ctx, container, name) - if err != nil { - panic(err) - } - - finder := find.NewFinder(vms.VIM()) - ds, err := finder.Datastore(ctx, container) - if err != nil { - return nil, err - } - - err = ds.Upload(ctx, &upload, path, &p) - if err != nil { - return nil, err - } - - err = vms.cloneImage(ctx, "/vmfs/volumes/nas011/iso/lunar-server-cloudimg-amd64.vmdk", fmt.Sprintf("/vmfs/volumes/%s/%s/%s.vmdk", container, name, name), diskSize) - if err != nil { - return nil, err - } - - err = vms.registerVM(ctx, fmt.Sprintf("/vmfs/volumes/%s/%s/%s.vmx", container, name, name)) - if err != nil { - return nil, err - } - - vm, err := vms.Get(ctx, name) - return vm, err -} - -func (vms *VirtualMachines) Destroy(ctx context.Context, name string) error { - vm, err := vms.Get(ctx, name) - if err != nil { - return err - } - - err = vm.PowerOff(ctx) - if err != nil { - return err - } - - // delete the content - datastore, dir, err := vm.InternalPath() - if err != nil { - return err - } - - err = vms.unregisterVM(ctx, vm.Id()) - if err != nil { - return err - } - - err = vms.wipe(ctx, datastore, dir) - - return err -} - -func (vms *VirtualMachines) unregisterVM(ctx context.Context, id string) error { - cmd := fmt.Sprintf("vim-cmd vmsvc/unregister %s", id) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: vms.sshAuth.User, - Password: vms.sshAuth.Password, - Host: vms.sshAuth.Host, - }, cmd) - if err != nil { - fmt.Printf("cmd: %s\n", cmd) - return err - } - return nil -} - -func (vms *VirtualMachines) registerVM(ctx context.Context, vmxPath string) error { - cmd := fmt.Sprintf("vim-cmd solo/registervm %s", vmxPath) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: vms.sshAuth.User, - Password: vms.sshAuth.Password, - Host: vms.sshAuth.Host, - }, cmd) - if err != nil { - fmt.Printf("cmd: %s\n", cmd) - return err - } - return nil -} - -func (vms *VirtualMachines) cloneImage(ctx context.Context, src, dest string, size uint) error { - cmd := fmt.Sprintf("vmkfstools --clonevirtualdisk %s --diskformat thin %s", src, dest) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: vms.sshAuth.User, - Password: vms.sshAuth.Password, - Host: vms.sshAuth.Host, - }, cmd) - if err != nil { - fmt.Printf("cmd: %s\n", cmd) - return err - } - - cmd = fmt.Sprintf("vmkfstools -X %s %s", "20G", dest) - _, err = sshutil.CombinedOutput(sshutil.Auth{ - User: vms.sshAuth.User, - Password: vms.sshAuth.Password, - Host: vms.sshAuth.Host, - }, cmd) - if err != nil { - fmt.Printf("cmd: %s\n", cmd) - return err - } - - return nil -} - -func (vms *VirtualMachines) wipe(ctx context.Context, datastore, dir string) error { - if vms.sshAuth != nil { - cmd := fmt.Sprintf("rm -rf /vmfs/volumes/%s/%s", datastore, dir) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: vms.sshAuth.User, - Password: vms.sshAuth.Password, - Host: vms.sshAuth.Host, - }, cmd) - - fmt.Printf("cmd: %s\n", cmd) - return err - } else { - panic("no ssh config") - } - - return nil - -} - -func (vms *VirtualMachines) Mkdir(ctx context.Context, datastore, dir string) error { - // you need a ESX payed license to create directories ... thats some bullshit */ - if vms.sshAuth != nil { - cmd := fmt.Sprintf("mkdir -p /vmfs/volumes/%s/%s", datastore, dir) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: vms.sshAuth.User, - Password: vms.sshAuth.Password, - Host: vms.sshAuth.Host, - }, cmd) - - fmt.Printf("cmd: %s\n", cmd) - return err - } else { - panic("no ssh config") - } - - return nil -} diff --git a/cmd/serviced/main.go b/cmd/serviced/main.go new file mode 100644 index 0000000..46157b5 --- /dev/null +++ b/cmd/serviced/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "esxlib/service" +) + +func main() { + err := service.Run("server.json") + if err != nil { + panic(err) + } +} diff --git a/cmd/vmctl/command.go b/cmd/vmctl/command.go new file mode 100644 index 0000000..edc7ee4 --- /dev/null +++ b/cmd/vmctl/command.go @@ -0,0 +1,62 @@ +package main + +import ( + "esxlib/api/cloud" + + "github.com/urfave/cli/v2" +) + +var ( + apiAddressFlag = cli.StringFlag{ + Name: "api.address", + Value: "127.0.0.1:1213", + Usage: "The address of the API server", + } + + apiTokenFlag = cli.StringFlag{ + Name: "api.token", + Value: "", + Usage: "The api token for the api if required", + } +) + +var globalFlags = []cli.Flag{ + &apiAddressFlag, + &apiTokenFlag, +} + +var commands = []*cli.Command{ + { + Name: "vm", + Aliases: []string{"vm"}, + Usage: "vm commands", + Description: "vm commands", + Subcommands: vmCommands, + }, + { + Name: "zones", + Aliases: []string{"zone"}, + Usage: "zone commands", + Description: "zone commands", + Subcommands: zoneCommands, + }, + { + Name: "networks", + Aliases: []string{"net", "nets"}, + Usage: "network commands", + Description: "network commands", + Subcommands: netCommands, + }, +} + +func mustGetClient(ctx *cli.Context) *cloud.Client { + apiAddress := ctx.String(apiAddressFlag.Name) + apiToken := ctx.String(apiTokenFlag.Name) + + client, err := cloud.NewClient(apiAddress, apiToken, true) + if err != nil { + panic(err) + } + + return client +} diff --git a/cmd/vmctl/command_network.go b/cmd/vmctl/command_network.go new file mode 100644 index 0000000..d6d5cf6 --- /dev/null +++ b/cmd/vmctl/command_network.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "os" + + "esxlib/api/cloud" + "esxlib/pkg/pprint" + + "github.com/urfave/cli/v2" +) + +var netCommands = []*cli.Command{ + { + Name: "list", + Aliases: []string{"ls", "list"}, + Usage: "list networks", + Description: "list networks", + Flags: []cli.Flag{ + &zoneIdFlag, + }, + Action: netCommandList, + }, +} + +func netCommandList(ctx *cli.Context) error { + client := mustGetClient(ctx) + + resp, err := client.NetworksList(context.Background(), &cloud.NetworksListRequest{ + ZoneId: ctx.String(zoneIdFlag.Name), + }) + if err != nil { + return err + } + + out := pprint.NewPrinter("human", os.Stdout) + + out.Headers("ID", "Name", "VLAN") + for _, item := range resp.Networks { + out.Fields(item.Id, item.Name, item.VLAN) + } + out.Flush() + return nil +} diff --git a/cmd/vmctl/command_vm.go b/cmd/vmctl/command_vm.go new file mode 100644 index 0000000..b5b8e79 --- /dev/null +++ b/cmd/vmctl/command_vm.go @@ -0,0 +1,255 @@ +package main + +import ( + "context" + "fmt" + "os" + + "esxlib/api/cloud" + "esxlib/pkg/pprint" + + "github.com/urfave/cli/v2" +) + +var ( + vmIdFlag = cli.StringFlag{ + Name: "vm.id", + Value: "", + Usage: "The id of the vm to fetch", + } + + vmNameFlag = cli.StringFlag{ + Name: "vm.name", + Value: "", + Required: true, + Usage: "The id of the vm to fetch", + } + + slugIdFlag = cli.StringFlag{ + Name: "slug.id", + Value: "", + Required: true, + Usage: "The id of the slug to use", + } + + sshKeyFileFlag = cli.StringFlag{ + Name: "ssh.hosts", + Value: "", + Usage: "SSH authorized host file to load keys from", + } + + cloudInitFileFlag = cli.StringFlag{ + Name: "cloudinit.file", + Value: "", + Usage: "CloudInit file to load", + } +) + +var vmCommands = []*cli.Command{ + { + Name: "list", + Aliases: []string{"ls", "list"}, + Usage: "list virtual machines", + Description: "list virtual machines", + Flags: []cli.Flag{}, + Action: vmCommandList, + }, + { + Name: "get", + Aliases: []string{"get"}, + Usage: "get details of a virtual machine", + Description: "get details of a virtual machines", + Flags: []cli.Flag{ + &vmIdFlag, + }, + Action: vmCommandGet, + }, + + { + Name: "power-on", + Aliases: []string{"poweron", "on", "resume"}, + Usage: "Power on/resume a Virtual machine", + Description: "Power on/resume a Virtual machine", + Flags: []cli.Flag{ + &vmIdFlag, + }, + Action: vmCommandPowerOn, + }, + { + Name: "power-off", + Aliases: []string{"poweroff", "off"}, + Usage: "Power off a Virtual machine", + Description: "Power off a Virtual machine", + Flags: []cli.Flag{ + &vmIdFlag, + }, + Action: vmCommandPowerOff, + }, + { + Name: "suspend", + Aliases: []string{"suspend"}, + Usage: "Suspend a Virtual machine", + Description: "Suspend a Virtual machine", + Flags: []cli.Flag{ + &vmIdFlag, + }, + Action: vmCommandPowerSuspend, + }, + { + Name: "destroy", + Aliases: []string{"destroy"}, + Usage: "Destroy an existing virtual machine", + Description: "Destroy an existing virtual machine", + Flags: []cli.Flag{ + &vmIdFlag, + }, + Action: vmCommandDestroy, + }, + + { + Name: "create", + Aliases: []string{"create"}, + Usage: "Create a new virtual machine", + Description: "Create a new virtual machine", + Flags: []cli.Flag{ + &vmNameFlag, + &zoneIdFlag, + &slugIdFlag, + &sshKeyFileFlag, + &cloudInitFileFlag, + }, + Action: vmCommandCreate, + }, +} + +func vmCommandList(ctx *cli.Context) error { + client := mustGetClient(ctx) + + resp, err := client.VMList(context.Background(), &cloud.VMListRequest{}) + if err != nil { + return err + } + + out := pprint.NewPrinter("human", os.Stdout) + + out.Headers("VMID", "Name", "State", "Address") + for _, vm := range resp.Vms { + address := "" + // just grab the last one (we can do better later + for _, a := range vm.Network { + address = a.Address + } + out.Fields(vm.Id, vm.Name, vm.State, address) + } + out.Flush() + return nil +} + +func vmCommandGet(ctx *cli.Context) error { + client := mustGetClient(ctx) + + resp, err := client.VMGet(context.Background(), &cloud.VMGetRequest{ + Search: &cloud.VMGetRequest_Id{ + Id: ctx.String(vmIdFlag.Name), + }, + }) + if err != nil { + return err + } + + out := pprint.NewPrinter("human", os.Stdout) + vm := resp.Vm + out.Headers("VMID", "Name", "State", "Address") + address := "" + // just grab the last one (we can do better later + for _, a := range vm.Network { + address = a.Address + } + out.Fields(vm.Id, vm.Name, vm.State, address) + out.Flush() + return nil +} + +func vmCommandPower(ctx *cli.Context, newState cloud.PowerState) error { + client := mustGetClient(ctx) + + _, err := client.VMPower(context.Background(), &cloud.VMPowerRequest{ + Id: ctx.String(vmIdFlag.Name), + State: newState, + }) + if err != nil { + return err + } + return nil +} + +func vmCommandPowerOn(ctx *cli.Context) error { + return vmCommandPower(ctx, cloud.PowerState_ON) +} + +func vmCommandPowerOff(ctx *cli.Context) error { + return vmCommandPower(ctx, cloud.PowerState_OFF) +} + +func vmCommandPowerSuspend(ctx *cli.Context) error { + return vmCommandPower(ctx, cloud.PowerState_SUSPENDED) +} + +func vmCommandCreate(ctx *cli.Context) error { + client := mustGetClient(ctx) + + sshAuthorizedHosts := "" + if ctx.String(sshKeyFileFlag.Name) != "" { + data, err := os.ReadFile(ctx.String(sshKeyFileFlag.Name)) + if err != nil { + return fmt.Errorf("Could not read hosts file: %s\n", ctx.String(sshKeyFileFlag.Name)) + } + sshAuthorizedHosts = string(data) + } + + cloudInit := "" + if ctx.String(cloudInitFileFlag.Name) != "" { + data, err := os.ReadFile(ctx.String(cloudInitFileFlag.Name)) + if err != nil { + return fmt.Errorf("Could not read cloud init file: %s\n", ctx.String(cloudInitFileFlag.Name)) + } + cloudInit = string(data) + } + + req := cloud.VMCreateRequest{ + Name: ctx.String(vmNameFlag.Name), + ZoneId: ctx.String(zoneIdFlag.Name), + Slug: ctx.String(slugIdFlag.Name), + InitScript: cloudInit, + AuthorizedHosts: []string{sshAuthorizedHosts}, + } + + resp, err := client.VMCreate(context.Background(), &req) + if err != nil { + return err + } + + out := pprint.NewPrinter("human", os.Stdout) + vm := resp.Vm + out.Headers("VMID", "Name", "State", "Address") + address := "" + // just grab the last one (we can do better later + for _, a := range vm.Network { + address = a.Address + } + out.Fields(vm.Id, vm.Name, vm.State, address) + out.Flush() + return nil +} + +func vmCommandDestroy(ctx *cli.Context) error { + client := mustGetClient(ctx) + _, err := client.VMDestroy(context.Background(), &cloud.VMDestroyRequest{ + Id: ctx.String(vmIdFlag.Name), + }) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/vmctl/command_zone.go b/cmd/vmctl/command_zone.go new file mode 100644 index 0000000..e57a901 --- /dev/null +++ b/cmd/vmctl/command_zone.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "os" + + "esxlib/api/cloud" + "esxlib/pkg/pprint" + + "github.com/urfave/cli/v2" +) + +var ( + zoneIdFlag = cli.StringFlag{ + Name: "zone.id", + Value: "", + Required: true, + Usage: "The zone id", + } +) + +var zoneCommands = []*cli.Command{ + { + Name: "list", + Aliases: []string{"ls", "list"}, + Usage: "list zones", + Description: "list zones", + Flags: []cli.Flag{}, + Action: zoneCommandList, + }, + + { + Name: "slugs", + Aliases: []string{"slugs"}, + Usage: "Lists slugs in a zone", + Description: "Lists slugs in a zone", + Flags: []cli.Flag{ + &zoneIdFlag, + }, + Action: zoneCommandSlugs, + }, +} + +func zoneCommandList(ctx *cli.Context) error { + client := mustGetClient(ctx) + + resp, err := client.ZonesList(context.Background(), &cloud.ZonesListRequest{}) + if err != nil { + return err + } + + out := pprint.NewPrinter("human", os.Stdout) + + out.Headers("ID", "Name") + for _, zone := range resp.Zones { + out.Fields(zone.Id, zone.Name) + } + out.Flush() + return nil +} + +func zoneCommandSlugs(ctx *cli.Context) error { + client := mustGetClient(ctx) + + resp, err := client.VMListSlugs(context.Background(), &cloud.VMListSlugsRequest{ + ZoneId: ctx.String(zoneIdFlag.Name), + }) + if err != nil { + return err + } + + out := pprint.NewPrinter("human", os.Stdout) + + out.Headers("ID", "Name", "Cpu", "memory", "Disk") + for _, item := range resp.Slugs { + out.Fields(item.Id, item.Name, item.CpuCount, item.MemoryMb, item.DiskGb) + } + out.Flush() + return nil +} diff --git a/cmd/vmctl/main.go b/cmd/vmctl/main.go index 8582ddc..2476bd6 100644 --- a/cmd/vmctl/main.go +++ b/cmd/vmctl/main.go @@ -1,91 +1,30 @@ package main import ( - "context" - "encoding/json" - "fmt" + "log" "os" - "time" - "esxlib/client" + "github.com/urfave/cli/v2" ) -type Config struct { - Host string - User string - Password string -} +const appName = "vmctl" -func UnmarshalFromFile(path string, v interface{}) error { - data, err := os.ReadFile(path) - if err != nil { - return err - } - - return json.Unmarshal(data, v) +func commandNotFound(c *cli.Context, command string) { + log.Fatalf("'%s' is not a %s command. See '%s --help'.", + command, c.App.Name, c.App.Name) } func main() { - config := &Config{} + app := cli.NewApp() + app.Name = appName + app.Description = "vm management tool for esx" + app.Version = "v1.0" - err := UnmarshalFromFile("config.json", config) - if err != nil { - panic(err) - } + app.Flags = globalFlags + app.Commands = commands + app.CommandNotFound = commandNotFound - url := fmt.Sprintf("https://%s:%s@%s", config.User, config.Password, config.Host) - c, err := client.NewClient( - context.TODO(), - url, - true, - client.WithSSH(fmt.Sprintf("%s:22", config.Host), config.User, config.Password)) - - if err != nil { - panic(err) - } - - ls, err := c.VirtualMachines.List(context.TODO()) - if err != nil { - panic(err) - } - - for _, vm := range ls { - fmt.Printf("%s\n", vm.Name()) - } - - vm, err := c.VirtualMachines.Create(context.TODO(), "test2", 1024, 20000, []byte("#!/bin/bash")) - if err != nil { - panic(err) - } - - fmt.Printf("%+v\n", vm) - - err = vm.PowerOn(context.TODO()) - if err != nil { - panic(err) - } - - for { - err = vm.Update() - if err != nil { - panic(err) - } - - addrs := vm.GetNetworkAddressV4() - if len(addrs) == 0 { - time.Sleep(time.Second * 5) - continue - } - - for _, a := range addrs { - fmt.Printf("%s\n", a) - } - break - } - - fmt.Printf("Destroy the vm now\n") - err = c.VirtualMachines.Destroy(context.TODO(), "test2") - if err != nil { - panic(err) + if err := app.Run(os.Args); err != nil { + log.Fatal(err) } } diff --git a/configs/ubuntu-lunar.cloudinit.tmpl b/configs/ubuntu-lunar.cloudinit.tmpl index f9955aa..3954088 100644 --- a/configs/ubuntu-lunar.cloudinit.tmpl +++ b/configs/ubuntu-lunar.cloudinit.tmpl @@ -1,20 +1,37 @@ #cloud-config -users: - - name: test - plain_text_passwd: test - groups: sudo - sudo: ALL=(ALL) NOPASSWD:ALL - shell: /bin/bash - lock_passwd: false +# See: the cloudinit cloud-config spec +hostname: {{ .Hostname }} + +# add some users... +#users: +# - name: test +# plain_text_passwd: test +# groups: sudo +# sudo: ALL=(ALL) NOPASSWD:ALL +# shell: /bin/bash +# lock_passwd: false + +# Not recommended, you should really create the VM with a root ssh key +#chpasswd: +# list: | +# root: somepassword +# expire: False -{{ if ne .InitScriptBase64 "" -}} write_files: +{{ if ne .InitScriptBase64 "" }}# Additional optional script to run in cloud init post - path: /root/.cloud-init-user.sh encoding: b64 content: {{ .InitScriptBase64 }} owner: root:root permissions: '0755' -{{ end }} +{{ end -}} +{{ if ne .AuthorizedKeys "" }}# Root Keys + - path: /root/.ssh/authorized_keys + encoding: b64 + content: {{ .AuthorizedKeys }} + owner: root:root + permissions: '0600' +{{ end -}} {{ if ne .InitScriptBase64 "" -}} runcmd: diff --git a/configs/ubuntu-lunar.esx.tmpl b/configs/ubuntu-lunar.esx.tmpl index 7844354..8c13a70 100644 --- a/configs/ubuntu-lunar.esx.tmpl +++ b/configs/ubuntu-lunar.esx.tmpl @@ -19,7 +19,7 @@ vmci0.present = "TRUE" hpet0.present = "TRUE" floppy0.present = "FALSE" RemoteDisplay.maxConnections = "-1" -numvcpus = "4" +numvcpus = "{{ .CpuCount }}" memSize = "{{ .MemSizeMB }}" bios.bootRetry.delay = "10" powerType.powerOff = "default" @@ -36,7 +36,7 @@ ehci.present = "TRUE" svga.autodetect = "TRUE" ethernet0.virtualDev = "vmxnet3" -ethernet0.networkName = "LAN" +ethernet0.networkName = "{{ .Network }}" ethernet0.addressType = "generated" ethernet0.wakeOnPcktRcv = "FALSE" ethernet0.uptCompatibility = "TRUE" diff --git a/esx/client.go b/esx/client.go new file mode 100644 index 0000000..aeb960b --- /dev/null +++ b/esx/client.go @@ -0,0 +1,155 @@ +package esx + +import ( + "context" + "fmt" + "regexp" + + "esxlib/pkg/sshutil" + "richat/pkg/utils" + + "github.com/vmware/govmomi/session/cache" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/soap" +) + +var diskRegexp = regexp.MustCompile("\\[(.*?)\\] (.*)") + +type Option func(c *Client) + +type Client struct { + sshAuth *sshutil.Auth + vim *vim25.Client + props *HostProperties + VirtualMachines VirtualMachines + Networks Networks +} + +type ClientRef interface { + VIM() *vim25.Client + hostExec(ctx context.Context, cmd string) error + hostProperties() *HostProperties +} + +func (c *Client) VIM() *vim25.Client { + return c.vim +} + +// hostExec runs a vim command using on the specific ssh host. +// Workaround: if you are running the FREE license of ESX, the power management API is not available to you +// IE: ServerFaultCode: Current license or ESXi version prohibits execution of the requested operation +// so as an alternative you can provide the SSH auth to the server and we do it the gross way +func (c *Client) hostExec(_ context.Context, cmd string) error { + if c.sshAuth == nil { + return fmt.Errorf("unsupported") + } + + _, err := sshutil.CombinedOutput(sshutil.Auth{ + User: c.sshAuth.User, + Password: c.sshAuth.Password, + Host: c.sshAuth.Host, + }, cmd) + + if err != nil { + fmt.Printf("cmd: %s err:%s\n", cmd, err.Error()) + return err + } + fmt.Printf("cmd: %s OK\n", cmd) + return nil +} + +func (c *Client) hostProperties() *HostProperties { + return c.props +} + +func WithSSH(host, user, password string) Option { + return func(c *Client) { + c.sshAuth = &sshutil.Auth{ + Host: host, + User: user, + Password: password, + } + } +} + +func WithHostProperties(props *HostProperties) Option { + return func(c *Client) { + c.props = props + } +} + +func NewClient(ctx context.Context, url string, insecure bool, opts ...Option) (*Client, error) { + // Parse URL from string + u, err := soap.ParseURL(url) + if err != nil { + return nil, err + } + + // Share govc's session cache + s := &cache.Session{ + URL: u, + Insecure: insecure, + } + + c := new(vim25.Client) + err = s.Login(ctx, c, nil) + if err != nil { + return nil, err + } + + client := &Client{ + vim: c, + props: &DefaultHostProperties, + } + + for _, opt := range opts { + opt(client) + } + + client.VirtualMachines.ClientRef = client + + return client, nil +} + +func NewClientFromConfigFile(configFile string) (*Client, error) { + config := &Config{} + + err := utils.UnmarshalFromFile(configFile, config) + if err != nil { + return nil, err + } + + return NewClientFromConfig(config) +} + +func NewClientFromConfig(config *Config) (*Client, error) { + vimURL, err := soap.ParseURL(fmt.Sprintf("https://%s:%s@%s", config.User, config.Password, config.Host)) + if err != nil { + return nil, err + } + + sessionCache := &cache.Session{ + URL: vimURL, + Insecure: true, + } + + c := new(vim25.Client) + err = sessionCache.Login(context.Background(), c, nil) + if err != nil { + return nil, err + } + + client := &Client{ + vim: c, + sshAuth: &sshutil.Auth{ + Host: fmt.Sprintf("%s:22", config.Host), + User: config.User, + Password: config.Password, + }, + props: &config.HostDefaults, + } + + client.VirtualMachines.ClientRef = client + client.Networks.ClientRef = client + return client, nil +} diff --git a/esx/config.go b/esx/config.go new file mode 100644 index 0000000..2c8d54b --- /dev/null +++ b/esx/config.go @@ -0,0 +1,9 @@ +package esx + +type Config struct { + Name string + Host string + User string + Password string + HostDefaults HostProperties +} diff --git a/esx/defaults.go b/esx/defaults.go new file mode 100644 index 0000000..761ea90 --- /dev/null +++ b/esx/defaults.go @@ -0,0 +1,65 @@ +package esx + +import ( + "fmt" + "path" +) + +var DefaultSlugs = map[string]Slug{ + "ubuntu-lunar-1gb-1cpu": { + Image: "ubuntu-lunar", + CpuCount: 1, + MemoryMB: 1024, + DiskSizeGB: 16, + }, +} + +var DefaultHostProperties = HostProperties{ + SlugSource: "/vmfs/volumes/datastore1/slugs", + DefaultDatastore: "datastore1", + DefaultNetwork: "LAN", + Slugs: DefaultSlugs, +} + +type Slug struct { + Id string + Image string + Description string + CpuCount uint + MemoryMB uint + DiskSizeGB uint + Cost uint +} + +type HostProperties struct { + Slugs map[string]Slug + SlugSource string + DefaultDatastore string + DefaultNetwork string +} + +func (hp *HostProperties) GetSlug(name string) (*Slug, error) { + if slug, ok := hp.Slugs[name]; ok { + return &slug, nil + } + return nil, fmt.Errorf("not found") +} + +func (hp *HostProperties) GetPathToSlug(slug Slug) string { + slugFilename := fmt.Sprintf("%s.cloudimg.amd64.vmdk", slug.Image) + return path.Join(hp.SlugSource, slugFilename) +} + +func (hp *HostProperties) GetPathToVM(name string) string { + return path.Join("/vmfs/volumes/", hp.DefaultDatastore, name) +} + +func (hp *HostProperties) GetPathToVMDisk(name string) string { + diskName := fmt.Sprintf("%s.vmdk", name) + return path.Join("/vmfs/volumes/", hp.DefaultDatastore, name, diskName) +} + +func (hp *HostProperties) GetPathToVMConfig(name string) string { + configName := fmt.Sprintf("%s.vmx", name) + return path.Join("/vmfs/volumes/", hp.DefaultDatastore, name, configName) +} diff --git a/esx/inventory_networks.go b/esx/inventory_networks.go new file mode 100644 index 0000000..ca8b233 --- /dev/null +++ b/esx/inventory_networks.go @@ -0,0 +1,57 @@ +package esx + +import ( + "context" + + "git.twelvetwelve.org/library/core/log" + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/property" + "github.com/vmware/govmomi/vim25/mo" +) + +type Networks struct { + ClientRef +} + +type Network struct { + Id string + Name string + VLAN int32 +} + +func (net *Networks) List(ctx context.Context) ([]Network, error) { + finder := find.NewFinder(net.VIM()) + + host, err := finder.DefaultHostSystem(ctx) + if err != nil { + log.Errorf("Could not find default host") + return nil, err + } + + ns, err := host.ConfigManager().NetworkSystem(ctx) + if err != nil { + log.Errorf("Could not get host config") + return nil, err + } + var mns mo.HostNetworkSystem + + pc := property.DefaultCollector(net.VIM()) + err = pc.RetrieveOne(ctx, ns.Reference(), []string{"networkInfo.portgroup"}, &mns) + if err != nil { + log.Errorf("Could not get portgroup config") + return nil, err + } + + networks := make([]Network, 0) + for _, pg := range mns.NetworkInfo.Portgroup { + networks = append(networks, Network{ + Id: pg.Spec.Name, + Name: pg.Spec.Name, + VLAN: pg.Spec.VlanId, + }) + + } + + // return mns.NetworkInfo.Portgroup, nil + return networks, nil +} diff --git a/esx/inventory_vms.go b/esx/inventory_vms.go new file mode 100644 index 0000000..a412e2a --- /dev/null +++ b/esx/inventory_vms.go @@ -0,0 +1,288 @@ +package esx + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "math/rand" + "strings" + "text/template" + "time" + + crypt "github.com/ncw/pwhash/sha512_crypt" + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/view" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" +) + +// VirtualMachines provides an interface fo retrieving virtual machines +type VirtualMachines struct { + ClientRef +} + +func encryptPassword(userPassword string) string { + // Generate a random string for use in the salt + const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + s := make([]byte, 8) + for i := range s { + s[i] = charset[seededRand.Intn(len(charset))] + } + salt := fmt.Sprintf("$6$%s", s) + // use salt to hash user-supplied password + hash := crypt.Crypt(userPassword, salt) + return string(hash) +} + +// List returns a list of registered Virtual Machines +func (vms *VirtualMachines) List(ctx context.Context) ([]*VirtualMachine, error) { + + m := view.NewManager(vms.VIM()) + v, err := m.CreateContainerView(ctx, vms.VIM().ServiceContent.RootFolder, []string{"VirtualMachine"}, true) + if err != nil { + return nil, err + } + defer v.Destroy(ctx) + + // Retrieve summary property for all machines + // Reference: http://pubs.vmware.com/vsphere-60/topic/com.vmware.wssdk.apiref.doc/vim.VirtualMachine.html + var moList []mo.VirtualMachine + err = v.Retrieve(ctx, []string{"VirtualMachine"}, nil, &moList) + if err != nil { + return nil, err + } + + vmList := make([]*VirtualMachine, 0) + + for idx := range moList { + vmList = append(vmList, &VirtualMachine{ + ClientRef: vms.ClientRef, + ref: moList[idx].Reference(), + mo: &moList[idx], + }) + } + + return vmList, nil +} + +// GetByName returns a virtual machine by name +func (vms *VirtualMachines) GetByName(ctx context.Context, name string) (*VirtualMachine, error) { + vmList, err := vms.List(ctx) + if err != nil { + return nil, err + } + + for idx, vm := range vmList { + if vm.Name() == name { + return vmList[idx], nil + } + } + + return nil, errors.New("vm does not exist") +} + +func (vms *VirtualMachines) GetById(ctx context.Context, id string) (*VirtualMachine, error) { + vmList, err := vms.List(ctx) + if err != nil { + return nil, err + } + + for idx, vm := range vmList { + if vm.Id() == id { + return vmList[idx], nil + } + } + + return nil, errors.New("vm does not exist") +} + +func (vms *VirtualMachines) CreateFromSlug(ctx context.Context, slug *Slug, name, password, sshKeys string, initScript []byte) (*VirtualMachine, error) { + path := fmt.Sprintf("%s/%s.vmx", name, name) + + initScriptEncoded := "" + if len(initScript) != 0 { + initScriptEncoded = base64.StdEncoding.EncodeToString(initScript) + } + sshKeysEncoded := "" + if sshKeys != "" { + sshKeysEncoded = base64.StdEncoding.EncodeToString([]byte(sshKeys)) + } + + tmpl, err := template.New("ubuntu-lunar.cloudinit.tmpl").ParseFiles("configs/ubuntu-lunar.cloudinit.tmpl") + if err != nil { + panic(err) + } + + config := struct { + Name string + Hostname string + DiskSizeGB uint + MemSizeMB uint + CpuCount uint + CloudInitBase64 string + InitScriptBase64 string + RootPassword string + AuthorizedKeys string + Network string + }{ + Name: name, + Hostname: name, + CpuCount: slug.CpuCount, + MemSizeMB: slug.MemoryMB, + DiskSizeGB: slug.DiskSizeGB, + InitScriptBase64: initScriptEncoded, + RootPassword: encryptPassword(password), + AuthorizedKeys: sshKeysEncoded, + Network: vms.hostProperties().DefaultNetwork, + } + + var b bytes.Buffer + w := bufio.NewWriter(&b) + + err = tmpl.Execute(w, config) + if err != nil { + panic(err) + } + err = w.Flush() + if err != nil { + panic(err) + } + + cloudInitEncoded := base64.StdEncoding.EncodeToString(b.Bytes()) + config.CloudInitBase64 = cloudInitEncoded + + fmt.Printf("%s\n", string(b.Bytes())) + + // Now encode the vm config + tmpl, err = template.New("ubuntu-lunar.esx.tmpl").ParseFiles("configs/ubuntu-lunar.esx.tmpl") + if err != nil { + panic(err) + } + + var upload bytes.Buffer + w = bufio.NewWriter(&upload) + + err = tmpl.Execute(w, config) + if err != nil { + panic(err) + } + err = w.Flush() + if err != nil { + panic(err) + } + + p := soap.DefaultUpload + + finder := find.NewFinder(vms.VIM()) + ds, err := finder.Datastore(ctx, vms.hostProperties().DefaultDatastore) + if err != nil { + return nil, err + } + + err = vms.createVMDirectory(ctx, vms.hostProperties().GetPathToVM(name)) + if err != nil { + return nil, err + } + + // if we fail at this point we need to do some cleanup + err = ds.Upload(ctx, &upload, path, &p) + if err != nil { + return nil, err + } + + err = vms.cloneImage(ctx, vms.hostProperties().GetPathToSlug(*slug), vms.hostProperties().GetPathToVMDisk(name), config.DiskSizeGB) + if err != nil { + return nil, err + } + + err = vms.registerVM(ctx, vms.hostProperties().GetPathToVMConfig(name)) + if err != nil { + return nil, err + } + + vm, err := vms.GetByName(ctx, name) + return vm, err + +} + +// Create a new Virtual machine from a slug +func (vms *VirtualMachines) Create(ctx context.Context, slugName string, name, sshKeys string, initScript []byte) (*VirtualMachine, error) { + slug, err := vms.hostProperties().GetSlug(slugName) + if err != nil { + return nil, err + } + + return vms.CreateFromSlug(ctx, slug, name, "", sshKeys, initScript) +} + +// Destroy the named virtual machine +func (vms *VirtualMachines) Destroy(ctx context.Context, id string) error { + vm, err := vms.GetById(ctx, id) + if err != nil { + return err + } + + if vm.State() != PowerStateOff { + err = vm.PowerOff(ctx) + if err != nil { + return err + } + } + + // delete the content + datastore, dir, err := vm.InternalPath() + if err != nil { + return err + } + + err = vms.unregisterVM(ctx, vm.Id()) + if err != nil { + return err + } + + err = vms.destroyVMDirectory(ctx, datastore, dir) + + return err +} + +func (vms *VirtualMachines) unregisterVM(ctx context.Context, id string) error { + cmd := fmt.Sprintf("vim-cmd vmsvc/unregister %s", id) + return vms.hostExec(ctx, cmd) +} + +func (vms *VirtualMachines) registerVM(ctx context.Context, vmxPath string) error { + cmd := fmt.Sprintf("vim-cmd solo/registervm %s", vmxPath) + return vms.hostExec(ctx, cmd) +} + +// cloneImage will copy a base image slug over to a new vm location +func (vms *VirtualMachines) cloneImage(ctx context.Context, src, dest string, sizeGB uint) error { + cmd := fmt.Sprintf("vmkfstools --clonevirtualdisk %s --diskformat thin %s", src, dest) + if err := vms.hostExec(ctx, cmd); err != nil { + return err + } + + cmd = fmt.Sprintf("vmkfstools -X %dG %s", sizeGB, dest) + if err := vms.hostExec(ctx, cmd); err != nil { + return err + } + return nil +} + +func (vms *VirtualMachines) destroyVMDirectory(ctx context.Context, datastore, dir string) error { + cmd := fmt.Sprintf("rm -rf /vmfs/volumes/%s/%s", datastore, dir) + return vms.hostExec(ctx, cmd) +} + +func (vms *VirtualMachines) createVMDirectory(ctx context.Context, dir string) error { + if !strings.HasPrefix(dir, "/vmfs/volumes/") { + return fmt.Errorf("mkdir got a bad path: %s", dir) + } + + cmd := fmt.Sprintf("mkdir %s", dir) + return vms.hostExec(ctx, cmd) +} diff --git a/client/vm.go b/esx/vm.go similarity index 50% rename from client/vm.go rename to esx/vm.go index f33bfa1..10d6b06 100644 --- a/client/vm.go +++ b/esx/vm.go @@ -1,4 +1,4 @@ -package client +package esx import ( "context" @@ -8,12 +8,7 @@ import ( "regexp" "strings" - "esxlib/pkg/sshutil" - - "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/property" - "github.com/vmware/govmomi/vim25" - "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" ) @@ -28,14 +23,14 @@ const ( ) type VirtualMachine struct { - c *vim25.Client - sshAuth *sshutil.Auth - mo *mo.VirtualMachine - ref types.ManagedObjectReference + ClientRef + //c *vim25.Client + mo *mo.VirtualMachine + ref types.ManagedObjectReference } func (v *VirtualMachine) refresh() error { - pc := property.DefaultCollector(v.c) + pc := property.DefaultCollector(v.VIM()) refs := []types.ManagedObjectReference{v.ref} var vms []mo.VirtualMachine @@ -92,6 +87,7 @@ func (v *VirtualMachine) GetNetworkAddressV4() []string { addresses := make([]string, 0) for _, x := range v.mo.Config.ExtraConfig { kv := x.GetOptionValue() + // fmt.Printf("%v:%v\n", kv.Key, kv.Value) switch kv.Key { case "guestinfo.local-ipv4": addresses = append(addresses, kv.Value.(string)) @@ -121,84 +117,21 @@ func (v *VirtualMachine) State() PowerState { } func (v *VirtualMachine) PowerOn(ctx context.Context) error { - if v.sshAuth != nil { - cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id()) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: v.sshAuth.User, - Password: v.sshAuth.Password, - Host: v.sshAuth.Host, - }, cmd) - return err - } - - return fmt.Errorf("unsupported (requires esx ssh privs)") + cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id()) + return v.hostExec(ctx, cmd) } func (v *VirtualMachine) PowerOff(ctx context.Context) error { - if v.sshAuth != nil { - cmd := fmt.Sprintf("vim-cmd vmsvc/power.off %s", v.Id()) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: v.sshAuth.User, - Password: v.sshAuth.Password, - Host: v.sshAuth.Host, - }, cmd) - return err - } - return fmt.Errorf("unsupported (requires esx ssh privs)") + cmd := fmt.Sprintf("vim-cmd vmsvc/power.off %s", v.Id()) + return v.hostExec(ctx, cmd) } func (v *VirtualMachine) Suspend(ctx context.Context) error { - // Workaround: if you are running the FREE license of ESX, the power management API is not available to you - // IE: ServerFaultCode: Current license or ESXi version prohibits execution of the requested operation - // so as an alternative you can provide the SSH auth to the server and we do it the gross way - if v.sshAuth != nil { - cmd := fmt.Sprintf("vim-cmd vmsvc/power.suspend %s", v.Id()) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: v.sshAuth.User, - Password: v.sshAuth.Password, - Host: v.sshAuth.Host, - }, cmd) - return err - } - - // standard API method - req := types.SuspendVM_Task{ - This: v.mo.Reference(), - } - - res, err := methods.SuspendVM_Task(ctx, v.c, &req) - if err != nil { - return err - } - - task := object.NewTask(v.c, res.Returnval) - return task.Wait(ctx) + cmd := fmt.Sprintf("vim-cmd vmsvc/power.suspend %s", v.Id()) + return v.hostExec(ctx, cmd) } func (v *VirtualMachine) Resume(ctx context.Context) error { - // Workaround: if you are running the FREE license of ESX, the power management API is not available to you - // IE: ServerFaultCode: Current license or ESXi version prohibits execution of the requested operation - // so as an alternative you can provide the SSH auth to the server and we do it the gross way - if v.sshAuth != nil { - cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id()) - _, err := sshutil.CombinedOutput(sshutil.Auth{ - User: v.sshAuth.User, - Password: v.sshAuth.Password, - Host: v.sshAuth.Host, - }, cmd) - return err - } - - // standard API method - req := types.PowerOnVM_Task{ - This: v.mo.Reference(), - } - - res, err := methods.PowerOnVM_Task(ctx, v.c, &req) - if err != nil { - return err - } - - task := object.NewTask(v.c, res.Returnval) - return task.Wait(ctx) + cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id()) + return v.hostExec(ctx, cmd) } diff --git a/go.mod b/go.mod index 06f2d2d..c31b72c 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,23 @@ module esxlib go 1.20 require ( + git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d + github.com/madflojo/testcerts v1.1.0 + github.com/ncw/pwhash v0.0.0-20160129162812-b2a8830c6a99 + github.com/urfave/cli/v2 v2.25.7 github.com/vmware/govmomi v0.30.4 golang.org/x/crypto v0.10.0 + google.golang.org/grpc v1.56.1 + google.golang.org/protobuf v1.30.0 ) -require golang.org/x/sys v0.9.0 // indirect +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect +) diff --git a/go.sum b/go.sum index d5b3724..b4dd55e 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,40 @@ +git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d h1:vZy7QM4kcGzbscnY1pSOGNDBDZMmhis/Z+9kZ70gD8Q= +git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d/go.mod h1:QSIAU+Arcx8nd7p4e3hFQIqi50cYf9I0KW9sqxkFnMY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/madflojo/testcerts v1.1.0 h1:kopRnZB2jH1yKhC2d27+GVQLDy/fL9/65UrkpJkF4GA= +github.com/madflojo/testcerts v1.1.0/go.mod h1:G+ucVds7Pj79qA9/ue9FygnXiBCm622IdzKWna621io= +github.com/ncw/pwhash v0.0.0-20160129162812-b2a8830c6a99 h1:OI8rVjsAwPQvzuDnzD5M/cw9vvv3IPtfg4o3OnWd3n4= +github.com/ncw/pwhash v0.0.0-20160129162812-b2a8830c6a99/go.mod h1:qnnNS25V653A+FZ6NpJvHABnYFvgOc2hLdaKKNKPgbI= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/vmware/govmomi v0.30.4 h1:BCKLoTmiBYRuplv3GxKEMBLtBaJm8PA56vo9bddIpYQ= github.com/vmware/govmomi v0.30.4/go.mod h1:F7adsVewLNHsW/IIm7ziFURaXDaHEwcc+ym4r3INMdY= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= +google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/pkg/pprint/csv.go b/pkg/pprint/csv.go new file mode 100644 index 0000000..393fbe8 --- /dev/null +++ b/pkg/pprint/csv.go @@ -0,0 +1,31 @@ +package pprint + +import ( + "fmt" + "io" +) + +type CSVPrinter struct { + writer io.Writer +} + +// Headers specify the table headers +func (p *CSVPrinter) Headers(headers ...string) { + for _, header := range headers { + fmt.Fprintf(p.writer, "%s,", header) + } + fmt.Fprintf(p.writer, "\n") +} + +// Fields add another row +func (p *CSVPrinter) Fields(fields ...interface{}) { + for _, header := range fields { + fmt.Fprintf(p.writer, "%v,", header) + } + fmt.Fprintf(p.writer, "\n") +} + +// Flush implements the flush interface but for CVS printer is a noop +func (p *CSVPrinter) Flush() { + // noop +} diff --git a/pkg/pprint/human.go b/pkg/pprint/human.go new file mode 100644 index 0000000..04230ca --- /dev/null +++ b/pkg/pprint/human.go @@ -0,0 +1,32 @@ +package pprint + +import ( + "fmt" + "text/tabwriter" +) + +// HumanPrinter prints tabulated data as a table +type HumanPrinter struct { + writer *tabwriter.Writer + fields int +} + +// Headers specify the table headers +func (p *HumanPrinter) Headers(headers ...string) { + for _, header := range headers { + fmt.Fprintf(p.writer, "%s\t", header) + } + fmt.Fprintf(p.writer, "\n") +} + +func (p *HumanPrinter) Fields(fields ...interface{}) { + for _, header := range fields { + fmt.Fprintf(p.writer, "%v\t", header) + } + fmt.Fprintf(p.writer, "\n") +} + +// Flush prints the data set +func (p *HumanPrinter) Flush() { + p.writer.Flush() +} diff --git a/pkg/pprint/printer.go b/pkg/pprint/printer.go new file mode 100644 index 0000000..9e11da0 --- /dev/null +++ b/pkg/pprint/printer.go @@ -0,0 +1,44 @@ +package pprint + +import ( + "io" + "os" + "text/tabwriter" +) + +// TabulatedPrinter defines an interface we can use for a common way to print tabulated data +// +// ie we canb use the same interface to display results as table, csv, json, etc +type TabulatedPrinter interface { + // Headers defines the headers for the values + Headers(...string) + + //Fields appends fields to the printer + Fields(...interface{}) + + // Flush outputs the tabulated data + Flush() +} + +// NewPrinter returns a new tabulated printer type based on the format specified +// format: can be human, csv +func NewPrinter(format string, w io.Writer) TabulatedPrinter { + if w == nil { + w = os.Stdout + } + + switch format { + case "csv": + return &CSVPrinter{ + writer: w, + } + + case "human": + p := &HumanPrinter{ + writer: new(tabwriter.Writer), + } + p.writer.Init(w, 0, 8, 2, ' ', 0) + return p + } + return nil +} diff --git a/pkg/utils/json.go b/pkg/utils/json.go new file mode 100644 index 0000000..b2672e7 --- /dev/null +++ b/pkg/utils/json.go @@ -0,0 +1,15 @@ +package utils + +import ( + "encoding/json" + "os" +) + +func UnmarshalFromFile(path string, v interface{}) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + return json.Unmarshal(data, v) +} diff --git a/server.json.example b/server.json.example new file mode 100644 index 0000000..36caca8 --- /dev/null +++ b/server.json.example @@ -0,0 +1,49 @@ +{ + "ListenAddress": "127.0.0.1:1213", + "Token": "", + "TLS": { + "KeyPath": "", + "CertPath": "" + }, + "Zones": { + "main": { + "Name": "Main ESX Server", + "Host": "10.0.0.2", + "User": "esxuser", + "Password": "esxpassword", + "HostDefaults": { + "DefaultNetwork": "Test", + "DefaultDatastore": "nvme1", + "SlugSource": "/vmfs/volumes/datastore1/slugs", + "Slugs": { + "ubuntu-lunar-1gb-1cpu": { + "Image": "ubuntu-lunar", + "CpuCount": 1, + "MemoryMB": 1024, + "DiskSizeGB": 16, + "Cost": 100 + } + } + } + }, + "lab": { + "Name": "LAB Esx Server", + "Host": "10.0.0.3", + "User": "esxuser", + "Password": "esxpassword", + "HostDefaults": { + "DefaultNetwork": "Test", + "DefaultDatastore": "nvme1", + "SlugSource": "/vmfs/volumes/datastore1/slugs", + "Slugs": { + "ubuntu-lunar-1gb-1cpu": { + "Image": "ubuntu-lunar", + "CpuCount": 1, + "MemoryMB": 1024, + "DiskSizeGB": 16, + "Cost": 100 + } + } + } + } +} diff --git a/service/config.go b/service/config.go new file mode 100644 index 0000000..d822f35 --- /dev/null +++ b/service/config.go @@ -0,0 +1,16 @@ +package service + +import ( + "esxlib/esx" +) + +type Config struct { + ListenAddress string + Token string + + TLS struct { + KeyPath string + CertPath string + } + Zones map[string]esx.Config +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..31b3f88 --- /dev/null +++ b/service/service.go @@ -0,0 +1,120 @@ +package service + +import ( + "context" + "fmt" + "net" + "os" + + "esxlib/api/cloud" + "esxlib/esx" + "esxlib/pkg/utils" + + "git.twelvetwelve.org/library/core/log" + "github.com/madflojo/testcerts" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type CloudServer struct { + cloud.UnimplementedCloudServer + esx map[string]*esx.Client + + config *Config +} + +var authorizedTokens = map[string]string{} + +func unaryInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + // Auth is not enabled on the server + if len(authorizedTokens) == 0 { + return handler(ctx, req) + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + log.Debugf("no metadata\n") + return nil, status.Errorf(codes.Unauthenticated, "Unauthenticated access") + } + + values := md["authorization"] + if len(values) == 0 { + log.Debugf("no authorization %+v\n", values) + return nil, status.Errorf(codes.Unauthenticated, "Unauthenticated access") + } + + for _, v := range values { + if _, ok := authorizedTokens[v]; ok { + return handler(ctx, req) + } + } + + log.Debugf("no authorization %+v\n", md) + return nil, status.Errorf(codes.Unauthenticated, "Unauthenticated access") +} + +func Run(configFile string) error { + config := &Config{} + + if err := utils.UnmarshalFromFile(configFile, config); err != nil { + return err + } + + esxClients := make(map[string]*esx.Client) + + for k := range config.Zones { + zone, _ := config.Zones[k] + + client, err := esx.NewClientFromConfig(&zone) + if err != nil { + return err + } + esxClients[k] = client + } + + lis, err := net.Listen("tcp", config.ListenAddress) + if err != nil { + panic(err) + } + + // TLS: Generate TLS if not provided + if config.TLS.CertPath == "" && config.TLS.KeyPath == "" { + config.TLS.CertPath, config.TLS.KeyPath, err = testcerts.GenerateCertsToTempFile("/tmp/") + if err != nil { + return err + } + fmt.Printf("Creating credentials... %s:%s\n", config.TLS.CertPath, config.TLS.KeyPath) + defer os.Remove(config.TLS.CertPath) + defer os.Remove(config.TLS.KeyPath) + } + + // generate local certs + creds, err := credentials.NewServerTLSFromFile(config.TLS.CertPath, config.TLS.KeyPath) + if err != nil { + log.Fatalf("Failed to setup TLS: %v\n", err) + } + + var grpcServer *grpc.Server + if config.Token != "" { + fmt.Printf("Need token: %s\n", config.Token) + authorizedTokens[config.Token] = config.Token + grpcServer = grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(unaryInterceptor)) + } else { + grpcServer = grpc.NewServer(grpc.Creds(creds)) + } + + cloudServer := CloudServer{ + esx: esxClients, + config: config, + } + + cloud.RegisterCloudServer(grpcServer, &cloudServer) + log.Printf("server listening at %v\n", lis.Addr()) + if err := grpcServer.Serve(lis); err != nil { + return err + } + return nil +} diff --git a/service/service_vm.go b/service/service_vm.go new file mode 100644 index 0000000..afbd6aa --- /dev/null +++ b/service/service_vm.go @@ -0,0 +1,223 @@ +package service + +import ( + "context" + "fmt" + "strings" + + "esxlib/api/cloud" + "esxlib/esx" + + "git.twelvetwelve.org/library/core/log" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func splitVmID(id string) (string, string) { + vals := strings.SplitN(id, ":", 2) + if len(vals) == 1 { + return "", "" + } + + // zone, id + return vals[0], vals[1] +} + +func vmToInfo(vm *esx.VirtualMachine, zoneId string) *cloud.VMInfo { + info := &cloud.VMInfo{ + Id: fmt.Sprintf("%s:%s", zoneId, vm.Id()), + Name: vm.Name(), + } + + switch vm.State() { + case esx.PowerStateOff: + info.State = cloud.PowerState_OFF + case esx.PowerStateOn: + info.State = cloud.PowerState_ON + case esx.PowerStateSuspended: + info.State = cloud.PowerState_SUSPENDED + case esx.PowerStateUndefined: + info.State = cloud.PowerState_UNKNOWN + } + + for _, vmNet := range vm.GetNetworkAddressV4() { + info.Network = append(info.Network, &cloud.VMNetworkInfo{ + Address: vmNet, + }) + } + + return info +} + +func (cs *CloudServer) VMList(ctx context.Context, req *cloud.VMListRequest) (*cloud.VMListResponse, error) { + resp := &cloud.VMListResponse{} + + for zoneId := range cs.esx { + fmt.Printf("Loading zone: %s\n", zoneId) + zone := cs.esx[zoneId] + + ls, err := zone.VirtualMachines.List(ctx) + if err != nil { + log.Errorf("VM from zone: %s failed err:%s\n", zoneId, err.Error()) + return nil, err + } + + for idx := range ls { + resp.Vms = append(resp.Vms, vmToInfo(ls[idx], zoneId)) + } + } + + return resp, nil +} + +func (cs *CloudServer) VMGet(ctx context.Context, req *cloud.VMGetRequest) (*cloud.VMGetResponse, error) { + switch search := req.Search.(type) { + case *cloud.VMGetRequest_Id: + zoneId, vmId := splitVmID(search.Id) + zone, ok := cs.esx[zoneId] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid vm id") + } + + vm, err := zone.VirtualMachines.GetById(ctx, vmId) + if err != nil { + return nil, err + } + + resp := &cloud.VMGetResponse{ + Vm: vmToInfo(vm, zoneId), + } + return resp, err + + } + return nil, status.Errorf(codes.Unimplemented, "method VMGet not implemented") +} + +func (cs *CloudServer) VMPower(ctx context.Context, req *cloud.VMPowerRequest) (*cloud.VMPowerResponse, error) { + zoneId, vmId := splitVmID(req.Id) + zone, ok := cs.esx[zoneId] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid vm id") + } + + vm, err := zone.VirtualMachines.GetById(ctx, vmId) + if err != nil { + return nil, err + } + + switch req.State { + case cloud.PowerState_OFF: + err = vm.PowerOff(ctx) + case cloud.PowerState_ON: + err = vm.PowerOn(ctx) + case cloud.PowerState_SUSPENDED: + err = vm.Suspend(ctx) + default: + return nil, fmt.Errorf("unsupported") + } + + if err != nil { + return nil, err + } + + return &cloud.VMPowerResponse{}, nil +} + +func (cs *CloudServer) VMCreate(ctx context.Context, req *cloud.VMCreateRequest) (*cloud.VMCreateResponse, error) { + zone, ok := cs.esx[req.ZoneId] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid vm id") + } + + var hosts string + for idx := range req.AuthorizedHosts { + hosts = hosts + req.AuthorizedHosts[idx] + "\n" + } + + vm, err := zone.VirtualMachines.Create(ctx, req.Slug, req.Name, hosts, []byte(req.InitScript)) + if err != nil { + return nil, err + } + + err = vm.PowerOn(ctx) + if err != nil { + log.Warnf("VM: %s:%s (%s) created but not powered on.", req.ZoneId, vm.Id(), vm.Name()) + } + _ = vm.Update() + + return &cloud.VMCreateResponse{ + Vm: vmToInfo(vm, req.ZoneId), + }, nil +} + +func (cs *CloudServer) VMDestroy(ctx context.Context, req *cloud.VMDestroyRequest) (*cloud.VMDestroyResponse, error) { + zoneId, vmId := splitVmID(req.Id) + zone, ok := cs.esx[zoneId] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid vm id") + } + + log.Eventf("VM Destroy: %s\n", req.Id) + err := zone.VirtualMachines.Destroy(ctx, vmId) + if err != nil { + return nil, err + } + log.Eventf("VM Destroy: %s OK\n", req.Id) + return &cloud.VMDestroyResponse{}, nil +} + +func (cs *CloudServer) VMListSlugs(ctx context.Context, req *cloud.VMListSlugsRequest) (*cloud.VMListSlugsResponse, error) { + zone, ok := cs.config.Zones[req.ZoneId] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid zone id") + } + + resp := &cloud.VMListSlugsResponse{} + for slugId, slug := range zone.HostDefaults.Slugs { + resp.Slugs = append(resp.Slugs, &cloud.Slug{ + Id: slugId, + Name: slug.Image, + Description: slug.Description, + CpuCount: uint32(slug.CpuCount), + MemoryMb: uint32(slug.MemoryMB), + DiskGb: uint32(slug.DiskSizeGB), + Cost: 0, + }) + } + + return resp, nil +} + +func (cs *CloudServer) ZonesList(ctx context.Context, req *cloud.ZonesListRequest) (*cloud.ZonesListResponse, error) { + resp := &cloud.ZonesListResponse{} + for zoneId, zone := range cs.config.Zones { + resp.Zones = append(resp.Zones, &cloud.Zone{ + Id: zoneId, + Name: zone.Name, + }) + } + return resp, nil +} + +func (cs *CloudServer) NetworksList(ctx context.Context, req *cloud.NetworksListRequest) (*cloud.NetworksListResponse, error) { + zone, ok := cs.esx[req.ZoneId] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "invalid zone id") + } + + networks, err := zone.Networks.List(ctx) + if err != nil { + return nil, err + } + + resp := &cloud.NetworksListResponse{} + for _, net := range networks { + resp.Networks = append(resp.Networks, &cloud.Network{ + Id: net.Id, + Name: net.Name, + VLAN: net.VLAN, + }) + } + + return resp, err +}