Initial server for cloudinit images

This commit is contained in:
spsobole 2023-06-24 13:57:08 -06:00
parent c706dc5126
commit 795fb3f916
32 changed files with 4057 additions and 529 deletions

11
Makefile Normal file
View File

@ -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

View File

@ -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.

38
api/cloud/client.go Normal file
View File

@ -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
}

1815
api/cloud/cloud.pb.go Normal file

File diff suppressed because it is too large Load Diff

139
api/cloud/cloud.proto Normal file
View File

@ -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;
}

368
api/cloud/cloud_grpc.pb.go Normal file
View File

@ -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",
}

View File

@ -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
}

View File

@ -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
}

12
cmd/serviced/main.go Normal file
View File

@ -0,0 +1,12 @@
package main
import (
"esxlib/service"
)
func main() {
err := service.Run("server.json")
if err != nil {
panic(err)
}
}

62
cmd/vmctl/command.go Normal file
View File

@ -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
}

View File

@ -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
}

255
cmd/vmctl/command_vm.go Normal file
View File

@ -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
}

80
cmd/vmctl/command_zone.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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:

View File

@ -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"

155
esx/client.go Normal file
View File

@ -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
}

9
esx/config.go Normal file
View File

@ -0,0 +1,9 @@
package esx
type Config struct {
Name string
Host string
User string
Password string
HostDefaults HostProperties
}

65
esx/defaults.go Normal file
View File

@ -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)
}

57
esx/inventory_networks.go Normal file
View File

@ -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
}

288
esx/inventory_vms.go Normal file
View File

@ -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)
}

View File

@ -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
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)")
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)")
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)
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)
return v.hostExec(ctx, cmd)
}

17
go.mod
View File

@ -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
View File

@ -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=

31
pkg/pprint/csv.go Normal file
View File

@ -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
}

32
pkg/pprint/human.go Normal file
View File

@ -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()
}

44
pkg/pprint/printer.go Normal file
View File

@ -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
}

15
pkg/utils/json.go Normal file
View File

@ -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)
}

49
server.json.example Normal file
View File

@ -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
}
}
}
}
}

16
service/config.go Normal file
View File

@ -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
}

120
service/service.go Normal file
View File

@ -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
}

223
service/service_vm.go Normal file
View File

@ -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
}