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
|
# 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"log"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"esxlib/client"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
const appName = "vmctl"
|
||||||
Host string
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalFromFile(path string, v interface{}) error {
|
func commandNotFound(c *cli.Context, command string) {
|
||||||
data, err := os.ReadFile(path)
|
log.Fatalf("'%s' is not a %s command. See '%s --help'.",
|
||||||
if err != nil {
|
command, c.App.Name, c.App.Name)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Unmarshal(data, v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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)
|
app.Flags = globalFlags
|
||||||
if err != nil {
|
app.Commands = commands
|
||||||
panic(err)
|
app.CommandNotFound = commandNotFound
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("https://%s:%s@%s", config.User, config.Password, config.Host)
|
if err := app.Run(os.Args); err != nil {
|
||||||
c, err := client.NewClient(
|
log.Fatal(err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,37 @@
|
||||||
#cloud-config
|
#cloud-config
|
||||||
users:
|
# See: the cloudinit cloud-config spec
|
||||||
- name: test
|
hostname: {{ .Hostname }}
|
||||||
plain_text_passwd: test
|
|
||||||
groups: sudo
|
# add some users...
|
||||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
#users:
|
||||||
shell: /bin/bash
|
# - name: test
|
||||||
lock_passwd: false
|
# 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:
|
write_files:
|
||||||
|
{{ if ne .InitScriptBase64 "" }}# Additional optional script to run in cloud init post
|
||||||
- path: /root/.cloud-init-user.sh
|
- path: /root/.cloud-init-user.sh
|
||||||
encoding: b64
|
encoding: b64
|
||||||
content: {{ .InitScriptBase64 }}
|
content: {{ .InitScriptBase64 }}
|
||||||
owner: root:root
|
owner: root:root
|
||||||
permissions: '0755'
|
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 "" -}}
|
{{ if ne .InitScriptBase64 "" -}}
|
||||||
runcmd:
|
runcmd:
|
||||||
|
|
|
@ -19,7 +19,7 @@ vmci0.present = "TRUE"
|
||||||
hpet0.present = "TRUE"
|
hpet0.present = "TRUE"
|
||||||
floppy0.present = "FALSE"
|
floppy0.present = "FALSE"
|
||||||
RemoteDisplay.maxConnections = "-1"
|
RemoteDisplay.maxConnections = "-1"
|
||||||
numvcpus = "4"
|
numvcpus = "{{ .CpuCount }}"
|
||||||
memSize = "{{ .MemSizeMB }}"
|
memSize = "{{ .MemSizeMB }}"
|
||||||
bios.bootRetry.delay = "10"
|
bios.bootRetry.delay = "10"
|
||||||
powerType.powerOff = "default"
|
powerType.powerOff = "default"
|
||||||
|
@ -36,7 +36,7 @@ ehci.present = "TRUE"
|
||||||
svga.autodetect = "TRUE"
|
svga.autodetect = "TRUE"
|
||||||
|
|
||||||
ethernet0.virtualDev = "vmxnet3"
|
ethernet0.virtualDev = "vmxnet3"
|
||||||
ethernet0.networkName = "LAN"
|
ethernet0.networkName = "{{ .Network }}"
|
||||||
ethernet0.addressType = "generated"
|
ethernet0.addressType = "generated"
|
||||||
ethernet0.wakeOnPcktRcv = "FALSE"
|
ethernet0.wakeOnPcktRcv = "FALSE"
|
||||||
ethernet0.uptCompatibility = "TRUE"
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -8,12 +8,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"esxlib/pkg/sshutil"
|
|
||||||
|
|
||||||
"github.com/vmware/govmomi/object"
|
|
||||||
"github.com/vmware/govmomi/property"
|
"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/mo"
|
||||||
"github.com/vmware/govmomi/vim25/types"
|
"github.com/vmware/govmomi/vim25/types"
|
||||||
)
|
)
|
||||||
|
@ -28,14 +23,14 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type VirtualMachine struct {
|
type VirtualMachine struct {
|
||||||
c *vim25.Client
|
ClientRef
|
||||||
sshAuth *sshutil.Auth
|
//c *vim25.Client
|
||||||
mo *mo.VirtualMachine
|
mo *mo.VirtualMachine
|
||||||
ref types.ManagedObjectReference
|
ref types.ManagedObjectReference
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VirtualMachine) refresh() error {
|
func (v *VirtualMachine) refresh() error {
|
||||||
pc := property.DefaultCollector(v.c)
|
pc := property.DefaultCollector(v.VIM())
|
||||||
refs := []types.ManagedObjectReference{v.ref}
|
refs := []types.ManagedObjectReference{v.ref}
|
||||||
|
|
||||||
var vms []mo.VirtualMachine
|
var vms []mo.VirtualMachine
|
||||||
|
@ -92,6 +87,7 @@ func (v *VirtualMachine) GetNetworkAddressV4() []string {
|
||||||
addresses := make([]string, 0)
|
addresses := make([]string, 0)
|
||||||
for _, x := range v.mo.Config.ExtraConfig {
|
for _, x := range v.mo.Config.ExtraConfig {
|
||||||
kv := x.GetOptionValue()
|
kv := x.GetOptionValue()
|
||||||
|
// fmt.Printf("%v:%v\n", kv.Key, kv.Value)
|
||||||
switch kv.Key {
|
switch kv.Key {
|
||||||
case "guestinfo.local-ipv4":
|
case "guestinfo.local-ipv4":
|
||||||
addresses = append(addresses, kv.Value.(string))
|
addresses = append(addresses, kv.Value.(string))
|
||||||
|
@ -121,84 +117,21 @@ func (v *VirtualMachine) State() PowerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VirtualMachine) PowerOn(ctx context.Context) error {
|
func (v *VirtualMachine) PowerOn(ctx context.Context) error {
|
||||||
if v.sshAuth != nil {
|
|
||||||
cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id())
|
cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id())
|
||||||
_, err := sshutil.CombinedOutput(sshutil.Auth{
|
return v.hostExec(ctx, cmd)
|
||||||
User: v.sshAuth.User,
|
|
||||||
Password: v.sshAuth.Password,
|
|
||||||
Host: v.sshAuth.Host,
|
|
||||||
}, cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("unsupported (requires esx ssh privs)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VirtualMachine) PowerOff(ctx context.Context) error {
|
func (v *VirtualMachine) PowerOff(ctx context.Context) error {
|
||||||
if v.sshAuth != nil {
|
|
||||||
cmd := fmt.Sprintf("vim-cmd vmsvc/power.off %s", v.Id())
|
cmd := fmt.Sprintf("vim-cmd vmsvc/power.off %s", v.Id())
|
||||||
_, err := sshutil.CombinedOutput(sshutil.Auth{
|
return v.hostExec(ctx, cmd)
|
||||||
User: v.sshAuth.User,
|
|
||||||
Password: v.sshAuth.Password,
|
|
||||||
Host: v.sshAuth.Host,
|
|
||||||
}, cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unsupported (requires esx ssh privs)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VirtualMachine) Suspend(ctx context.Context) error {
|
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())
|
cmd := fmt.Sprintf("vim-cmd vmsvc/power.suspend %s", v.Id())
|
||||||
_, err := sshutil.CombinedOutput(sshutil.Auth{
|
return v.hostExec(ctx, cmd)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VirtualMachine) Resume(ctx context.Context) error {
|
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())
|
cmd := fmt.Sprintf("vim-cmd vmsvc/power.on %s", v.Id())
|
||||||
_, err := sshutil.CombinedOutput(sshutil.Auth{
|
return v.hostExec(ctx, cmd)
|
||||||
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)
|
|
||||||
}
|
}
|
17
go.mod
17
go.mod
|
@ -3,8 +3,23 @@ module esxlib
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
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
|
github.com/vmware/govmomi v0.30.4
|
||||||
golang.org/x/crypto v0.10.0
|
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/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 h1:BCKLoTmiBYRuplv3GxKEMBLtBaJm8PA56vo9bddIpYQ=
|
||||||
github.com/vmware/govmomi v0.30.4/go.mod h1:F7adsVewLNHsW/IIm7ziFURaXDaHEwcc+ym4r3INMdY=
|
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 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
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 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
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