package driver import ( "context" "errors" "fmt" "log" "strings" "time" "github.com/hashicorp/packer/packer" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/nfc" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/ovf" "github.com/vmware/govmomi/property" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" ) type VirtualMachine struct { vm *object.VirtualMachine driver *Driver } type CloneConfig struct { Name string Folder string Cluster string Host string ResourcePool string Datastore string LinkedClone bool Network string Annotation string } type HardwareConfig struct { CPUs int32 CpuCores int32 CPUReservation int64 CPULimit int64 RAM int64 RAMReservation int64 RAMReserveAll bool NestedHV bool CpuHotAddEnabled bool MemoryHotAddEnabled bool VideoRAM int64 VGPUProfile string Firmware string } type NIC struct { Network string // "" for default network NetworkCard string // example: vmxnet3 MacAddress string // set mac if want specific address Passthrough *bool // direct path i/o } type CreateConfig struct { DiskControllerType string // example: "scsi", "pvscsi" Annotation string Name string Folder string Cluster string Host string ResourcePool string Datastore string GuestOS string // example: otherGuest NICs []NIC USBController bool Version uint // example: 10 Firmware string // efi-secure, efi or bios Storage []Disk } type Disk struct { DiskSize int64 DiskEagerlyScrub bool DiskThinProvisioned bool } func (d *Driver) NewVM(ref *types.ManagedObjectReference) *VirtualMachine { return &VirtualMachine{ vm: object.NewVirtualMachine(d.client.Client, *ref), driver: d, } } func (d *Driver) FindVM(name string) (*VirtualMachine, error) { vm, err := d.finder.VirtualMachine(d.ctx, name) if err != nil { return nil, err } return &VirtualMachine{ vm: vm, driver: d, }, nil } func (d *Driver) PreCleanVM(ui packer.Ui, vmPath string, force bool) error { vm, err := d.FindVM(vmPath) if err != nil { if _, ok := err.(*find.NotFoundError); !ok { return fmt.Errorf("error looking up old vm: %v", err) } } if force && vm != nil { ui.Say(fmt.Sprintf("the vm/template %s already exists, but deleting it due to -force flag", vmPath)) // power off just in case it is still on vm.PowerOff() err := vm.Destroy() if err != nil { return fmt.Errorf("error destroying %s: %v", vmPath, err) } } if !force && vm != nil { return fmt.Errorf("%s already exists, you can use -force flag to destroy it: %v", vmPath, err) } return nil } func (d *Driver) CreateVM(config *CreateConfig) (*VirtualMachine, error) { createSpec := types.VirtualMachineConfigSpec{ Name: config.Name, Annotation: config.Annotation, GuestId: config.GuestOS, } if config.Version != 0 { createSpec.Version = fmt.Sprintf("%s%d", "vmx-", config.Version) } if config.Firmware == "efi-secure" { createSpec.Firmware = "efi" createSpec.BootOptions = &types.VirtualMachineBootOptions{ EfiSecureBootEnabled: types.NewBool(true), } } else if config.Firmware != "" { createSpec.Firmware = config.Firmware } folder, err := d.FindFolder(config.Folder) if err != nil { return nil, err } resourcePool, err := d.FindResourcePool(config.Cluster, config.Host, config.ResourcePool) if err != nil { return nil, err } var host *object.HostSystem if config.Cluster != "" && config.Host != "" { h, err := d.FindHost(config.Host) if err != nil { return nil, err } host = h.host } datastore, err := d.FindDatastore(config.Datastore, config.Host) if err != nil { return nil, err } devices := object.VirtualDeviceList{} devices, err = addDisk(d, devices, config) if err != nil { return nil, err } devices, err = addNetwork(d, devices, config) if err != nil { return nil, err } if config.USBController { t := true usb := &types.VirtualUSBController{ EhciEnabled: &t, } devices = append(devices, usb) } createSpec.DeviceChange, err = devices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd) if err != nil { return nil, err } createSpec.Files = &types.VirtualMachineFileInfo{ VmPathName: fmt.Sprintf("[%s]", datastore.Name()), } task, err := folder.folder.CreateVM(d.ctx, createSpec, resourcePool.pool, host) if err != nil { return nil, err } taskInfo, err := task.WaitForResult(d.ctx, nil) if err != nil { return nil, err } vmRef := taskInfo.Result.(types.ManagedObjectReference) return d.NewVM(&vmRef), nil } func (vm *VirtualMachine) Info(params ...string) (*mo.VirtualMachine, error) { var p []string if len(params) == 0 { p = []string{"*"} } else { p = params } var info mo.VirtualMachine err := vm.vm.Properties(vm.driver.ctx, vm.vm.Reference(), p, &info) if err != nil { return nil, err } return &info, nil } func (vm *VirtualMachine) Devices() (object.VirtualDeviceList, error) { vmInfo, err := vm.Info("config.hardware.device") if err != nil { return nil, err } return vmInfo.Config.Hardware.Device, nil } func (vm *VirtualMachine) Clone(ctx context.Context, config *CloneConfig) (*VirtualMachine, error) { folder, err := vm.driver.FindFolder(config.Folder) if err != nil { return nil, err } var relocateSpec types.VirtualMachineRelocateSpec pool, err := vm.driver.FindResourcePool(config.Cluster, config.Host, config.ResourcePool) if err != nil { return nil, err } poolRef := pool.pool.Reference() relocateSpec.Pool = &poolRef datastore, err := vm.driver.FindDatastore(config.Datastore, config.Host) if err != nil { return nil, err } datastoreRef := datastore.ds.Reference() relocateSpec.Datastore = &datastoreRef var cloneSpec types.VirtualMachineCloneSpec cloneSpec.Location = relocateSpec cloneSpec.PowerOn = false if config.LinkedClone == true { cloneSpec.Location.DiskMoveType = "createNewChildDiskBacking" tpl, err := vm.Info("snapshot") if err != nil { return nil, err } if tpl.Snapshot == nil { err = errors.New("`linked_clone=true`, but template has no snapshots") return nil, err } cloneSpec.Snapshot = tpl.Snapshot.CurrentSnapshot } var configSpec types.VirtualMachineConfigSpec cloneSpec.Config = &configSpec if config.Annotation != "" { configSpec.Annotation = config.Annotation } if config.Network != "" { net, err := vm.driver.FindNetwork(config.Network) if err != nil { return nil, err } backing, err := net.network.EthernetCardBackingInfo(ctx) if err != nil { return nil, err } devices, err := vm.vm.Device(ctx) if err != nil { return nil, err } adapter, err := findNetworkAdapter(devices) if err != nil { return nil, err } adapter.GetVirtualEthernetCard().Backing = backing config := &types.VirtualDeviceConfigSpec{ Device: adapter.(types.BaseVirtualDevice), Operation: types.VirtualDeviceConfigSpecOperationEdit, } configSpec.DeviceChange = append(configSpec.DeviceChange, config) } task, err := vm.vm.Clone(vm.driver.ctx, folder.folder, config.Name, cloneSpec) if err != nil { return nil, err } info, err := task.WaitForResult(ctx, nil) if err != nil { if ctx.Err() == context.Canceled { err = task.Cancel(context.TODO()) return nil, err } return nil, err } vmRef := info.Result.(types.ManagedObjectReference) created := vm.driver.NewVM(&vmRef) return created, nil } func (vm *VirtualMachine) Destroy() error { task, err := vm.vm.Destroy(vm.driver.ctx) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) Configure(config *HardwareConfig) error { var confSpec types.VirtualMachineConfigSpec confSpec.NumCPUs = config.CPUs confSpec.NumCoresPerSocket = config.CpuCores confSpec.MemoryMB = config.RAM var cpuSpec types.ResourceAllocationInfo cpuSpec.Reservation = &config.CPUReservation if config.CPULimit != 0 { cpuSpec.Limit = &config.CPULimit } confSpec.CpuAllocation = &cpuSpec var ramSpec types.ResourceAllocationInfo ramSpec.Reservation = &config.RAMReservation confSpec.MemoryAllocation = &ramSpec confSpec.MemoryReservationLockedToMax = &config.RAMReserveAll confSpec.NestedHVEnabled = &config.NestedHV confSpec.CpuHotAddEnabled = &config.CpuHotAddEnabled confSpec.MemoryHotAddEnabled = &config.MemoryHotAddEnabled if config.VideoRAM != 0 { devices, err := vm.vm.Device(vm.driver.ctx) if err != nil { return err } l := devices.SelectByType((*types.VirtualMachineVideoCard)(nil)) if len(l) != 1 { return err } card := l[0].(*types.VirtualMachineVideoCard) card.VideoRamSizeInKB = config.VideoRAM spec := &types.VirtualDeviceConfigSpec{ Device: card, Operation: types.VirtualDeviceConfigSpecOperationEdit, } confSpec.DeviceChange = append(confSpec.DeviceChange, spec) } if config.VGPUProfile != "" { devices, err := vm.vm.Device(vm.driver.ctx) if err != nil { return err } pciDevices := devices.SelectByType((*types.VirtualPCIPassthrough)(nil)) vGPUDevices := pciDevices.SelectByBackingInfo((*types.VirtualPCIPassthroughVmiopBackingInfo)(nil)) var operation types.VirtualDeviceConfigSpecOperation if len(vGPUDevices) > 1 { return err } else if len(pciDevices) == 1 { operation = types.VirtualDeviceConfigSpecOperationEdit } else if len(pciDevices) == 0 { operation = types.VirtualDeviceConfigSpecOperationAdd } vGPUProfile := newVGPUProfile(config.VGPUProfile) spec := &types.VirtualDeviceConfigSpec{ Device: &vGPUProfile, Operation: operation, } log.Printf("Adding vGPU device with profile '%s'", config.VGPUProfile) confSpec.DeviceChange = append(confSpec.DeviceChange, spec) } if config.Firmware == "efi-secure" || config.Firmware == "efi" { confSpec.Firmware = "efi" confSpec.BootOptions = &types.VirtualMachineBootOptions{ EfiSecureBootEnabled: types.NewBool(config.Firmware == "efi-secure"), } } else if config.Firmware != "" { confSpec.Firmware = config.Firmware } task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) ResizeDisk(diskSize int64) error { var confSpec types.VirtualMachineConfigSpec devices, err := vm.vm.Device(vm.driver.ctx) if err != nil { return err } disk, err := findDisk(devices) if err != nil { return err } disk.CapacityInKB = diskSize * 1024 confSpec.DeviceChange = []types.BaseVirtualDeviceConfigSpec{ &types.VirtualDeviceConfigSpec{ Device: disk, Operation: types.VirtualDeviceConfigSpecOperationEdit, }, } task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func findDisk(devices object.VirtualDeviceList) (*types.VirtualDisk, error) { var disks []*types.VirtualDisk for _, device := range devices { switch d := device.(type) { case *types.VirtualDisk: disks = append(disks, d) } } switch len(disks) { case 0: return nil, errors.New("VM has no disks") case 1: return disks[0], nil } return nil, errors.New("VM has multiple disks") } func (vm *VirtualMachine) PowerOn() error { task, err := vm.vm.PowerOn(vm.driver.ctx) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) WaitForIP(ctx context.Context) (string, error) { return vm.vm.WaitForIP(ctx) } func (vm *VirtualMachine) PowerOff() error { state, err := vm.vm.PowerState(vm.driver.ctx) if err != nil { return err } if state == types.VirtualMachinePowerStatePoweredOff { return nil } task, err := vm.vm.PowerOff(vm.driver.ctx) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) IsPoweredOff() (bool, error) { state, err := vm.vm.PowerState(vm.driver.ctx) if err != nil { return false, err } return state == types.VirtualMachinePowerStatePoweredOff, nil } func (vm *VirtualMachine) StartShutdown() error { err := vm.vm.ShutdownGuest(vm.driver.ctx) return err } func (vm *VirtualMachine) WaitForShutdown(ctx context.Context, timeout time.Duration) error { shutdownTimer := time.After(timeout) for { off, err := vm.IsPoweredOff() if err != nil { return err } if off { break } select { case <-shutdownTimer: err := errors.New("Timeout while waiting for machine to shut down.") return err case <-ctx.Done(): return nil default: time.Sleep(1 * time.Second) } } return nil } func (vm *VirtualMachine) CreateSnapshot(name string) error { task, err := vm.vm.CreateSnapshot(vm.driver.ctx, name, "", false, false) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) ConvertToTemplate() error { return vm.vm.MarkAsTemplate(vm.driver.ctx) } func (vm *VirtualMachine) GetDir() (string, error) { vmInfo, err := vm.Info("name", "layoutEx.file") if err != nil { return "", err } vmxName := fmt.Sprintf("/%s.vmx", vmInfo.Name) for _, file := range vmInfo.LayoutEx.File { if strings.HasSuffix(file.Name, vmxName) { return RemoveDatastorePrefix(file.Name[:len(file.Name)-len(vmxName)]), nil } } return "", fmt.Errorf("cannot find '%s'", vmxName) } func addDisk(_ *Driver, devices object.VirtualDeviceList, config *CreateConfig) (object.VirtualDeviceList, error) { if len(config.Storage) == 0 { return nil, errors.New("no storage devices have been defined") } device, err := devices.CreateSCSIController(config.DiskControllerType) if err != nil { return nil, err } devices = append(devices, device) controller, err := devices.FindDiskController(devices.Name(device)) if err != nil { return nil, err } for _, dc := range config.Storage { disk := &types.VirtualDisk{ VirtualDevice: types.VirtualDevice{ Key: devices.NewKey(), Backing: &types.VirtualDiskFlatVer2BackingInfo{ DiskMode: string(types.VirtualDiskModePersistent), ThinProvisioned: types.NewBool(dc.DiskThinProvisioned), EagerlyScrub: types.NewBool(dc.DiskEagerlyScrub), }, }, CapacityInKB: dc.DiskSize * 1024, } devices.AssignController(disk, controller) devices = append(devices, disk) } return devices, nil } func addNetwork(d *Driver, devices object.VirtualDeviceList, config *CreateConfig) (object.VirtualDeviceList, error) { if len(config.NICs) == 0 { return nil, errors.New("no network adapters have been defined") } var network object.NetworkReference for _, nic := range config.NICs { if nic.Network == "" { h, err := d.FindHost(config.Host) if err != nil { return nil, err } i, err := h.Info("network") if err != nil { return nil, err } if len(i.Network) > 1 { return nil, fmt.Errorf("Host has multiple networks. Specify it explicitly") } network = object.NewNetwork(d.client.Client, i.Network[0]) } else { var err error network, err = d.finder.Network(d.ctx, nic.Network) if err != nil { return nil, err } } backing, err := network.EthernetCardBackingInfo(d.ctx) if err != nil { return nil, err } device, err := object.EthernetCardTypes().CreateEthernetCard(nic.NetworkCard, backing) if err != nil { return nil, err } card := device.(types.BaseVirtualEthernetCard).GetVirtualEthernetCard() if nic.MacAddress != "" { card.AddressType = string(types.VirtualEthernetCardMacTypeManual) card.MacAddress = nic.MacAddress } card.UptCompatibilityEnabled = nic.Passthrough devices = append(devices, device) } return devices, nil } func newVGPUProfile(vGPUProfile string) types.VirtualPCIPassthrough { return types.VirtualPCIPassthrough{ VirtualDevice: types.VirtualDevice{ DeviceInfo: &types.Description{ Summary: "", Label: fmt.Sprintf("New vGPU %v PCI device", vGPUProfile), }, Backing: &types.VirtualPCIPassthroughVmiopBackingInfo{ Vgpu: vGPUProfile, }, }, } } func (vm *VirtualMachine) AddCdrom(controllerType string, isoPath string) error { devices, err := vm.vm.Device(vm.driver.ctx) if err != nil { return err } var controller *types.VirtualController if controllerType == "sata" { c, err := vm.FindSATAController() if err != nil { return err } controller = c.GetVirtualController() } else { c, err := devices.FindIDEController("") if err != nil { return err } controller = c.GetVirtualController() } cdrom, err := vm.CreateCdrom(controller) if err != nil { return err } if isoPath != "" { devices.InsertIso(cdrom, isoPath) } log.Printf("Creating CD-ROM on controller '%v' with iso '%v'", controller, isoPath) return vm.addDevice(cdrom) } func (vm *VirtualMachine) AddFloppy(imgPath string) error { devices, err := vm.vm.Device(vm.driver.ctx) if err != nil { return err } floppy, err := devices.CreateFloppy() if err != nil { return err } if imgPath != "" { floppy = devices.InsertImg(floppy, imgPath) } return vm.addDevice(floppy) } func (vm *VirtualMachine) SetBootOrder(order []string) error { devices, err := vm.vm.Device(vm.driver.ctx) if err != nil { return err } bootOptions := types.VirtualMachineBootOptions{ BootOrder: devices.BootOrder(order), } return vm.vm.SetBootOptions(vm.driver.ctx, &bootOptions) } func (vm *VirtualMachine) RemoveDevice(keepFiles bool, device ...types.BaseVirtualDevice) error { return vm.vm.RemoveDevice(vm.driver.ctx, keepFiles, device...) } func (vm *VirtualMachine) addDevice(device types.BaseVirtualDevice) error { newDevices := object.VirtualDeviceList{device} confSpec := types.VirtualMachineConfigSpec{} var err error confSpec.DeviceChange, err = newDevices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd) if err != nil { return err } task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) AddConfigParams(params map[string]string) error { var confSpec types.VirtualMachineConfigSpec var ov []types.BaseOptionValue for k, v := range params { o := types.OptionValue{ Key: k, Value: v, } ov = append(ov, &o) } confSpec.ExtraConfig = ov task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) if err != nil { return err } _, err = task.WaitForResult(vm.driver.ctx, nil) return err } func (vm *VirtualMachine) Export() (*nfc.Lease, error) { return vm.vm.Export(vm.driver.ctx) } func (vm *VirtualMachine) CreateDescriptor(m *ovf.Manager, cdp types.OvfCreateDescriptorParams) (*types.OvfCreateDescriptorResult, error) { return m.CreateDescriptor(vm.driver.ctx, vm.vm, cdp) } func (vm *VirtualMachine) NewOvfManager() *ovf.Manager { return ovf.NewManager(vm.vm.Client()) } func (vm *VirtualMachine) GetOvfExportOptions(m *ovf.Manager) ([]types.OvfOptionInfo, error) { var mgr mo.OvfManager err := property.DefaultCollector(vm.vm.Client()).RetrieveOne(vm.driver.ctx, m.Reference(), nil, &mgr) if err != nil { return nil, err } return mgr.OvfExportOption, nil } func findNetworkAdapter(l object.VirtualDeviceList) (types.BaseVirtualEthernetCard, error) { c := l.SelectByType((*types.VirtualEthernetCard)(nil)) if len(c) == 0 { return nil, errors.New("no network adapter device found") } return c[0].(types.BaseVirtualEthernetCard), nil }