pkg/virtualizers/vmware/virtualizer.go
package vmware
/**
* SPDX-License-Identifier: Apache-2.0
* Copyright 2020 vorteil.io Pty Ltd
*/
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/thanhpk/randstr"
"github.com/vorteil/vorteil/pkg/elog"
"github.com/vorteil/vorteil/pkg/vcfg"
"github.com/vorteil/vorteil/pkg/vio"
"github.com/vorteil/vorteil/pkg/virtualizers"
logger "github.com/vorteil/vorteil/pkg/virtualizers/logging"
"github.com/vorteil/vorteil/pkg/virtualizers/util"
)
// vmwareType workstation by default switch to fusion when on a darwin system
var vmwareType = "workstation"
func init() {
if runtime.GOOS == "darwin" {
vmwareType = "fusion"
}
}
// Virtualizer is a struct which will implement the interface so the manager can create VMs
type Virtualizer struct {
id string // unique hash for pipe and folder names.
name string // name of the vm
pname string // name of virtualizer spawned from
state string // the state of the vm
headless bool // bool to show or not to show the gui
created time.Time // time the vm was created
folder string // path to the folder containing vmx, disk for vm
disk *os.File // the disk the vm is running
vmxPath string // the vmx file workstation will use
networkType string // the type of network the vm spawns on
source interface{} //details about how the source was created using api.source struct
serialLogger *logger.Logger // serial output logger for app that gets run
startCommand *exec.Cmd // The execute command to start the vmware instance
sock net.Conn // net connection to read serial from
logger elog.View // logger for the CLI
routes []virtualizers.NetworkInterface
config *vcfg.VCFG
vmdrive string // store disks in this directory
}
// RemoveEntry from vmware inventory
func (v *Virtualizer) RemoveEntry() error {
env, err := os.UserHomeDir()
if err != nil {
return err
}
if runtime.GOOS == "windows" {
env = os.Getenv("APPDATA")
}
pathVMware := filepath.ToSlash(filepath.Join(env, "VMware/inventory.vmls"))
if runtime.GOOS != "windows" {
pathVMware = filepath.ToSlash(filepath.Join(env, ".vmware/inventory.vmls"))
}
file, err := ioutil.ReadFile(pathVMware)
if err != nil {
return err
}
keys := make([]string, 0)
found := false
// Fetch what lines i need to remove from the file
lines := strings.Split(string(file), "\n")
for _, line := range lines {
if strings.Contains(line, v.vmxPath) {
id := strings.TrimSpace(strings.Split(line, "=")[0])
removeType := strings.Split(id, ".")[0]
keys = append(keys, removeType)
found = true
}
}
// if not found under .vorteild directory try with normal spot does not need to happen on windows as vmware is always open
if !found && runtime.GOOS != "windows" {
pathVMware = filepath.ToSlash(filepath.Join(filepath.Dir(env), ".vmware/inventory.vmls"))
file, err = ioutil.ReadFile(pathVMware)
if err != nil {
return err
}
// Fetch what lines i need to remove from the file
lines := strings.Split(string(file), "\n")
for _, line := range lines {
if strings.Contains(line, v.vmxPath) {
id := strings.TrimSpace(strings.Split(line, "=")[0])
removeType := strings.Split(id, ".")[0]
keys = append(keys, removeType)
}
}
}
// open the file for editing.
f, err := os.Create(pathVMware)
if err != nil {
return err
}
for _, line := range lines {
lineFound := false
for _, key := range keys {
if strings.HasPrefix(line, key) {
lineFound = true
}
}
// check index.count line to adjust it with removing one
if strings.HasPrefix(line, "index.count") {
count := strings.TrimSpace(strings.Split(line, "=")[1])
count = strings.Trim(count, "\"")
// remove one from index as were deleting one vm from vmware
ncount, err := strconv.Atoi(count)
if err != nil {
return err
}
ncount--
f.WriteString(fmt.Sprintf("index.count = \"%s\"", strconv.Itoa(ncount)))
lineFound = true
}
// If line didn't hit any checks write the file back in as usual
if !lineFound {
f.WriteString(line)
}
}
defer f.Close()
return nil
}
// Close deletes and cleans up the VM
func (v *Virtualizer) Close(force bool) error {
v.logger.Debugf("Deleting VM")
if force && v.state != virtualizers.Ready {
err := v.ForceStop()
if err != nil {
return err
}
}
if v.state != virtualizers.Ready {
err := v.Stop()
if err != nil {
return err
}
}
command := exec.Command("vmrun", "-T", vmwareType, "deleteVM", v.vmxPath)
output, err := v.execute(command)
if err != nil {
if !strings.Contains(err.Error(), "4294967295") && !strings.Contains(err.Error(), "3221225786") {
if runtime.GOOS == "darwin" && !v.headless {
if strings.Contains(err.Error(), "is in use") {
v.logger.Errorf("%s (if running with gui make sure its closed)", err.Error())
return fmt.Errorf("%s (if running with gui make sure its closed)", err.Error())
}
}
return err
}
}
if len(output) > 0 {
v.logger.Debugf("%s", output)
}
v.state = virtualizers.Deleted
if v.sock != nil {
v.sock.Close()
}
if !v.headless {
err = v.RemoveEntry()
if err != nil {
// the gui on mac requires you to remove it before you can delete so returning this error makes no sense
if runtime.GOOS != "darwin" {
return err
}
}
}
virtualizers.ActiveVMs.Delete(v.name)
return nil
}
// Detach removes vm from active vm list
// func (v *Virtualizer) Detach(source string) error {
// if v.state != virtualizers.Ready {
// return errors.New("virtual machine must be in ready state to detach")
// }
// err := os.MkdirAll(filepath.Join(source, v.name), 0777)
// if err != nil {
// return err
// }
// cmd := exec.Command("vmrun", "-T", vmwareType, "clone", v.vmxPath, filepath.Join(source, v.name, filepath.Base(v.vmxPath)), "full")
// _, err = v.execute(cmd)
// if err != nil {
// if strings.Contains(err.Error(), "4294967295") {
// return errors.New("vm contents already exist at location")
// }
// return err
// }
// command := exec.Command("vmrun", "-T", vmwareType, "deleteVM", v.vmxPath)
// output, err := v.execute(command)
// if err != nil {
// if !strings.Contains(err.Error(), "4294967295") {
// if runtime.GOOS == "darwin" && !v.headless {
// if strings.Contains(err.Error(), "is in use") {
// v.logger.Errorf("%s (if running with gui make sure its closed)", err.Error())
// return fmt.Errorf("%s (if running with gui make sure its closed)", err.Error())
// }
// }
// return err
// }
// }
// if len(output) > 0 {
// v.logger.Debugf("%s", output)
// }
// v.state = virtualizers.Deleted
// if v.sock != nil {
// v.sock.Close()
// }
// v.disk.Close()
// virtualizers.ActiveVMs.Delete(v.name)
// // err = os.RemoveAll(v.folder)
// // if err != nil {
// // return err
// // }
// return nil
// }
// ForceStop stop the vm without shutting down mainly used when the daemon gets powered off
func (v *Virtualizer) ForceStop() error {
command := exec.Command("vmrun", "-T", vmwareType, "stop", v.vmxPath, "hard")
output, err := v.execute(command)
if err != nil {
if !strings.Contains(err.Error(), "4294967295") {
return err
}
}
if len(output) > 0 {
v.logger.Debugf("%s", output)
}
v.state = virtualizers.Ready
return nil
}
// Stop the vm with sigint through the hypervisor
func (v *Virtualizer) Stop() error {
v.logger.Debugf("Stopping VM")
if v.state != virtualizers.Ready {
v.state = virtualizers.Changing
command := exec.Command("vmrun", "-T", vmwareType, "stop", v.vmxPath)
output, err := v.execute(command)
if err != nil {
if !strings.Contains(err.Error(), "4294967295") && !strings.Contains(err.Error(), "3221225786") {
return err
}
}
if len(output) > 0 {
v.logger.Debugf("%s", output)
}
v.state = virtualizers.Ready
}
return nil
}
// execute is a generic wrapper function for executing commands
func (v *Virtualizer) execute(cmd *exec.Cmd) (string, error) {
v.logger.Infof("Executing %s", cmd.Args)
resp, err := cmd.CombinedOutput()
if err != nil {
if err.Error() == "" || err.Error() == "exit status 255" {
return "", errors.New(string(resp))
}
return "", err
}
output := string(resp)
return output, nil
}
// Start the vm
func (v *Virtualizer) Start() error {
v.logger.Debugf("Starting VM")
v.startCommand = exec.Command(v.startCommand.Args[0], v.startCommand.Args[1:]...)
switch v.State() {
case "ready":
go v.initLogs()
output, err := v.execute(v.startCommand)
if err != nil {
if !strings.Contains(err.Error(), "3221225786") {
v.logger.Errorf("Error starting vm: %v", err)
return err
}
}
if len(output) > 0 {
v.logger.Debugf("%s", output)
}
go func() {
v.routes = util.LookForIP(v.serialLogger, v.routes)
v.state = virtualizers.Alive
}()
go v.checkRunning()
default:
return fmt.Errorf("cannot start vm in state '%s'", v.State())
}
return nil
}
// Serial returns the serial logger
func (v *Virtualizer) Serial() *logger.Logger {
return v.serialLogger
}
// State returns the state of the virtual machine
func (v *Virtualizer) State() string {
return v.state
}
// Type returns the type of the virtualizer
func (v *Virtualizer) Type() string {
return VirtualizerID
}
// Initialize creates the virtualizer and appends needed data from the Config
func (v *Virtualizer) Initialize(data []byte) error {
c := new(Config)
err := c.Unmarshal(data)
if err != nil {
return err
}
v.networkType = c.NetworkType
v.headless = c.Headless
return nil
}
// operation is the job progress that gets tracked via APIs
type operation struct {
finishedLock sync.Mutex
isFinished bool
*Virtualizer
Logs chan string
Status chan string
Error chan error
ctx context.Context
}
// log writes a log to the channel for the job
func (o *operation) log(text string, v ...interface{}) {
o.Logs <- fmt.Sprintf(text, v...)
}
// finished completes the operation and lets the user know and cleans up channels
func (o *operation) finished(err error) {
o.finishedLock.Lock()
defer o.finishedLock.Unlock()
if o.isFinished {
return
}
o.isFinished = true
if err != nil {
o.Logs <- fmt.Sprintf("Error: %v", err)
o.Status <- fmt.Sprintf("Failed: %v", err)
o.Error <- err
}
close(o.Logs)
close(o.Status)
close(o.Error)
}
// updateStatus updates the status of the job to provide more feedback to the user currently reading the job.
func (o *operation) updateStatus(text string) {
o.Status <- text
o.Logs <- text
}
// Prepare sets the fields and arguments to spawn the virtual machine
func (v *Virtualizer) Prepare(args *virtualizers.PrepareArgs) *virtualizers.VirtualizeOperation {
op := new(operation)
v.name = args.Name
v.pname = args.PName
v.vmdrive = args.VMDrive
v.created = time.Now()
v.config = args.Config
v.logger = args.Logger
v.source = args.Source
v.serialLogger = logger.NewLogger(2048 * 10)
v.routes = util.Routes(args.Config.Networks)
v.logger.Debugf("Preparing VM")
op.Logs = make(chan string, 128)
op.Error = make(chan error, 1)
op.Status = make(chan string, 10)
op.ctx = args.Context
op.Virtualizer = v
o := new(virtualizers.VirtualizeOperation)
o.Logs = op.Logs
o.Error = op.Error
o.Status = op.Status
go op.prepare(args)
return o
}
// Download returns the disk as a vio.File
func (v *Virtualizer) Download() (vio.File, error) {
v.logger.Debugf("Downloading Disk")
if !(v.state == virtualizers.Ready) {
return nil, fmt.Errorf("the machine must be in a stopped or ready state")
}
f, err := vio.LazyOpen(v.disk.Name())
if err != nil {
return nil, err
}
return f, nil
}
// Details returns data to for the ConverToVM function on util
func (v *Virtualizer) Details() (string, string, string, []virtualizers.NetworkInterface, time.Time, *vcfg.VCFG, interface{}) {
return v.name, v.pname, v.state, v.routes, v.created, v.config, v.source
}
// prepare sets the fields and arguments to spawn the virtual machine
func (o *operation) prepare(args *virtualizers.PrepareArgs) {
var returnErr error
o.updateStatus(fmt.Sprintf("Preparing VMware...."))
defer func() {
o.finished(returnErr)
}()
executable, err := virtualizers.GetExecutable(VirtualizerID)
if err != nil {
returnErr = err
return
}
o.state = "initializing"
o.id = randstr.Hex(5)
o.folder = filepath.Dir(args.ImagePath)
o.config.VM.RAM.Align(vcfg.MiB * 4)
vmxString := GenerateVMX(strconv.Itoa(int(o.config.VM.CPUs)), strconv.Itoa(o.config.VM.RAM.Units(vcfg.MiB)), args.ImagePath, o.name, o.folder, len(o.routes), o.networkType, o.id)
vmxPath := filepath.Join(o.folder, o.name+".vmx")
o.vmxPath = vmxPath
err = ioutil.WriteFile(vmxPath, []byte(vmxString), os.ModePerm)
if err != nil {
returnErr = err
return
}
argsC := []string{"-T", vmwareType, "start", o.vmxPath}
if o.headless {
argsC = append(argsC, "nogui")
}
o.startCommand = exec.Command(executable, argsC...)
_, loaded := virtualizers.ActiveVMs.LoadOrStore(o.name, o.Virtualizer)
if loaded {
returnErr = errors.New("virtual machine already exists")
return
}
o.state = "ready"
if args.Start {
err = o.Start()
if err != nil {
returnErr = fmt.Errorf("Error starting vm: %v", err)
return
}
}
}
// Poll to check if its still running
func (v *Virtualizer) checkRunning() {
for {
running, err := v.isRunning()
if err != nil {
if !strings.Contains(err.Error(), "3221225786") {
v.logger.Errorf("Checking Running State: %s", err)
return
}
}
if !running {
v.state = virtualizers.Ready
break
}
time.Sleep(time.Second * 1)
}
}
// Checks if the vm is still running as vmrun does not come with state management.
func (v *Virtualizer) isRunning() (bool, error) {
running := false
command := exec.Command("vmrun", "list")
var errS bytes.Buffer
command.Stdout = &errS
err := command.Run()
if err != nil {
return running, err
}
output := fmt.Sprint(command.Stdout)
lines := strings.Split(output, "\n")
vms, _ := strconv.Atoi(strings.Split(string(lines[0]), ": ")[1])
// try with carriage return for windows
if vms == 0 {
lines = strings.Split(string(output), "\r\n")
vms, _ = strconv.Atoi(strings.Split(string(lines[0]), ": ")[1])
}
vmxFile, err := os.Stat(v.vmxPath)
if err != nil {
return running, err
}
for i := 0; i < vms; i++ {
vmxFile2, err := os.Stat(lines[i+1])
if err != nil {
continue
}
if os.SameFile(vmxFile, vmxFile2) {
running = true
break
}
continue
}
return running, nil
}