Initial server for cloudinit images
This commit is contained in:
parent
c706dc5126
commit
795fb3f916
|
@ -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
|
24
README.md
24
README.md
|
@ -1,3 +1,25 @@
|
|||
# esxlib
|
||||
|
||||
Library/Service to interact with ESX server.
|
||||
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.
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"esxlib/service"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := service.Run("server.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package esx
|
||||
|
||||
type Config struct {
|
||||
Name string
|
||||
Host string
|
||||
User string
|
||||
Password string
|
||||
HostDefaults HostProperties
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
17
go.mod
17
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
|
||||
)
|
||||
|
|
32
go.sum
32
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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue