package vsphere import ( "fmt" "log" "net" "strconv" "strings" "github.com/hashicorp/terraform/helper/schema" "github.com/vmware/govmomi" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/property" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" "golang.org/x/net/context" ) var DefaultDNSSuffixes = []string{ "vsphere.local", } var DefaultDNSServers = []string{ "8.8.8.8", "8.8.4.4", } type networkInterface struct { deviceName string label string ipv4Address string ipv4PrefixLength int ipv4Gateway string ipv6Address string ipv6PrefixLength int ipv6Gateway string adapterType string // TODO: Make "adapter_type" argument } type hardDisk struct { name string size int64 iops int64 initType string vmdkPath string controller string bootable bool } //Additional options Vsphere can use clones of windows machines type windowsOptConfig struct { productKey string adminPassword string domainUser string domain string domainUserPassword string } type cdrom struct { datastore string path string } type memoryAllocation struct { reservation int64 } type virtualMachine struct { name string folder string datacenter string cluster string resourcePool string datastore string vcpu int32 memoryMb int64 memoryAllocation memoryAllocation template string networkInterfaces []networkInterface hardDisks []hardDisk cdroms []cdrom domain string timeZone string dnsSuffixes []string dnsServers []string hasBootableVmdk bool linkedClone bool skipCustomization bool windowsOptionalConfig windowsOptConfig customConfigurations map[string](types.AnyType) } func (v virtualMachine) Path() string { return vmPath(v.folder, v.name) } func vmPath(folder string, name string) string { var path string if len(folder) > 0 { path += folder + "/" } return path + name } func resourceVSphereVirtualMachine() *schema.Resource { return &schema.Resource{ Create: resourceVSphereVirtualMachineCreate, Read: resourceVSphereVirtualMachineRead, Update: resourceVSphereVirtualMachineUpdate, Delete: resourceVSphereVirtualMachineDelete, Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "folder": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "vcpu": &schema.Schema{ Type: schema.TypeInt, Required: true, }, "memory": &schema.Schema{ Type: schema.TypeInt, Required: true, }, "memory_reservation": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: 0, ForceNew: true, }, "datacenter": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "cluster": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "resource_pool": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "linked_clone": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, ForceNew: true, }, "gateway": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, Deprecated: "Please use network_interface.ipv4_gateway", }, "domain": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, Default: "vsphere.local", }, "time_zone": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, Default: "Etc/UTC", }, "dns_suffixes": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, ForceNew: true, }, "dns_servers": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, ForceNew: true, }, "skip_customization": &schema.Schema{ Type: schema.TypeBool, Optional: true, ForceNew: true, Default: false, }, "custom_configuration_parameters": &schema.Schema{ Type: schema.TypeMap, Optional: true, ForceNew: true, }, "windows_opt_config": &schema.Schema{ Type: schema.TypeList, Optional: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "product_key": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "admin_password": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "domain_user": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "domain": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, "domain_user_password": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, }, }, }, "network_interface": &schema.Schema{ Type: schema.TypeList, Required: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "label": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "ip_address": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, Deprecated: "Please use ipv4_address", }, "subnet_mask": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, Deprecated: "Please use ipv4_prefix_length", }, "ipv4_address": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, }, "ipv4_prefix_length": &schema.Schema{ Type: schema.TypeInt, Optional: true, Computed: true, }, "ipv4_gateway": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, }, "ipv6_address": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, }, "ipv6_prefix_length": &schema.Schema{ Type: schema.TypeInt, Optional: true, Computed: true, }, "ipv6_gateway": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, }, "adapter_type": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, }, }, }, "disk": &schema.Schema{ Type: schema.TypeSet, Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "uuid": &schema.Schema{ Type: schema.TypeString, Computed: true, }, "key": &schema.Schema{ Type: schema.TypeInt, Computed: true, }, "template": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "type": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "eager_zeroed", ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) if value != "thin" && value != "eager_zeroed" { errors = append(errors, fmt.Errorf( "only 'thin' and 'eager_zeroed' are supported values for 'type'")) } return }, }, "datastore": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "size": &schema.Schema{ Type: schema.TypeInt, Optional: true, }, "name": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "iops": &schema.Schema{ Type: schema.TypeInt, Optional: true, }, "vmdk": &schema.Schema{ // TODO: Add ValidateFunc to confirm path exists Type: schema.TypeString, Optional: true, }, "bootable": &schema.Schema{ Type: schema.TypeBool, Optional: true, }, "keep_on_remove": &schema.Schema{ Type: schema.TypeBool, Optional: true, }, "controller_type": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "scsi", ForceNew: true, ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) if value != "scsi" && value != "ide" { errors = append(errors, fmt.Errorf( "only 'scsi' and 'ide' are supported values for 'controller_type'")) } return }, }, }, }, }, "cdrom": &schema.Schema{ Type: schema.TypeList, Optional: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "datastore": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "path": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, }, }, }, }, } } func resourceVSphereVirtualMachineUpdate(d *schema.ResourceData, meta interface{}) error { // flag if changes have to be applied hasChanges := false // flag if changes have to be done when powered off rebootRequired := false // make config spec configSpec := types.VirtualMachineConfigSpec{} if d.HasChange("vcpu") { configSpec.NumCPUs = int32(d.Get("vcpu").(int)) hasChanges = true rebootRequired = true } if d.HasChange("memory") { configSpec.MemoryMB = int64(d.Get("memory").(int)) hasChanges = true rebootRequired = true } client := meta.(*govmomi.Client) dc, err := getDatacenter(client, d.Get("datacenter").(string)) if err != nil { return err } finder := find.NewFinder(client.Client, true) finder = finder.SetDatacenter(dc) vm, err := finder.VirtualMachine(context.TODO(), vmPath(d.Get("folder").(string), d.Get("name").(string))) if err != nil { return err } if d.HasChange("disk") { hasChanges = true oldDisks, newDisks := d.GetChange("disk") oldDiskSet := oldDisks.(*schema.Set) newDiskSet := newDisks.(*schema.Set) addedDisks := newDiskSet.Difference(oldDiskSet) removedDisks := oldDiskSet.Difference(newDiskSet) // Removed disks for _, diskRaw := range removedDisks.List() { if disk, ok := diskRaw.(map[string]interface{}); ok { devices, err := vm.Device(context.TODO()) if err != nil { return fmt.Errorf("[ERROR] Update Remove Disk - Could not get virtual device list: %v", err) } virtualDisk := devices.FindByKey(int32(disk["key"].(int))) keep := false if v, ok := d.GetOk("keep_on_remove"); ok { keep = v.(bool) } err = vm.RemoveDevice(context.TODO(), keep, virtualDisk) if err != nil { return fmt.Errorf("[ERROR] Update Remove Disk - Error removing disk: %v", err) } } } // Added disks for _, diskRaw := range addedDisks.List() { if disk, ok := diskRaw.(map[string]interface{}); ok { var datastore *object.Datastore if disk["datastore"] == "" { datastore, err = finder.DefaultDatastore(context.TODO()) if err != nil { return fmt.Errorf("[ERROR] Update Remove Disk - Error finding datastore: %v", err) } } else { datastore, err = finder.Datastore(context.TODO(), disk["datastore"].(string)) if err != nil { log.Printf("[ERROR] Couldn't find datastore %v. %s", disk["datastore"].(string), err) return err } } var size int64 if disk["size"] == 0 { size = 0 } else { size = int64(disk["size"].(int)) } iops := int64(disk["iops"].(int)) controller_type := disk["controller"].(string) var mo mo.VirtualMachine vm.Properties(context.TODO(), vm.Reference(), []string{"summary", "config"}, &mo) var diskPath string switch { case disk["vmdk"] != "": diskPath = disk["vmdk"].(string) case disk["name"] != "": snapshotFullDir := mo.Config.Files.SnapshotDirectory split := strings.Split(snapshotFullDir, " ") if len(split) != 2 { return fmt.Errorf("[ERROR] createVirtualMachine - failed to split snapshot directory: %v", snapshotFullDir) } vmWorkingPath := split[1] diskPath = vmWorkingPath + disk["name"].(string) default: return fmt.Errorf("[ERROR] resourceVSphereVirtualMachineUpdate - Neither vmdk path nor vmdk name was given") } log.Printf("[INFO] Attaching disk: %v", diskPath) err = addHardDisk(vm, size, iops, "thin", datastore, diskPath, controller_type) if err != nil { log.Printf("[ERROR] Add Hard Disk Failed: %v", err) return err } } if err != nil { return err } } } // do nothing if there are no changes if !hasChanges { return nil } log.Printf("[DEBUG] virtual machine config spec: %v", configSpec) if rebootRequired { log.Printf("[INFO] Shutting down virtual machine: %s", d.Id()) task, err := vm.PowerOff(context.TODO()) if err != nil { return err } err = task.Wait(context.TODO()) if err != nil { return err } } log.Printf("[INFO] Reconfiguring virtual machine: %s", d.Id()) task, err := vm.Reconfigure(context.TODO(), configSpec) if err != nil { log.Printf("[ERROR] %s", err) } err = task.Wait(context.TODO()) if err != nil { log.Printf("[ERROR] %s", err) } if rebootRequired { task, err = vm.PowerOn(context.TODO()) if err != nil { return err } err = task.Wait(context.TODO()) if err != nil { log.Printf("[ERROR] %s", err) } } ip, err := vm.WaitForIP(context.TODO()) if err != nil { return err } log.Printf("[DEBUG] ip address: %v", ip) return resourceVSphereVirtualMachineRead(d, meta) } func resourceVSphereVirtualMachineCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*govmomi.Client) vm := virtualMachine{ name: d.Get("name").(string), vcpu: int32(d.Get("vcpu").(int)), memoryMb: int64(d.Get("memory").(int)), memoryAllocation: memoryAllocation{ reservation: int64(d.Get("memory_reservation").(int)), }, } if v, ok := d.GetOk("folder"); ok { vm.folder = v.(string) } if v, ok := d.GetOk("datacenter"); ok { vm.datacenter = v.(string) } if v, ok := d.GetOk("cluster"); ok { vm.cluster = v.(string) } if v, ok := d.GetOk("resource_pool"); ok { vm.resourcePool = v.(string) } if v, ok := d.GetOk("domain"); ok { vm.domain = v.(string) } if v, ok := d.GetOk("time_zone"); ok { vm.timeZone = v.(string) } if v, ok := d.GetOk("linked_clone"); ok { vm.linkedClone = v.(bool) } if v, ok := d.GetOk("skip_customization"); ok { vm.skipCustomization = v.(bool) } if raw, ok := d.GetOk("dns_suffixes"); ok { for _, v := range raw.([]interface{}) { vm.dnsSuffixes = append(vm.dnsSuffixes, v.(string)) } } else { vm.dnsSuffixes = DefaultDNSSuffixes } if raw, ok := d.GetOk("dns_servers"); ok { for _, v := range raw.([]interface{}) { vm.dnsServers = append(vm.dnsServers, v.(string)) } } else { vm.dnsServers = DefaultDNSServers } if vL, ok := d.GetOk("custom_configuration_parameters"); ok { if custom_configs, ok := vL.(map[string]interface{}); ok { custom := make(map[string]types.AnyType) for k, v := range custom_configs { custom[k] = v } vm.customConfigurations = custom log.Printf("[DEBUG] custom_configuration_parameters init: %v", vm.customConfigurations) } } if vL, ok := d.GetOk("network_interface"); ok { networks := make([]networkInterface, len(vL.([]interface{}))) for i, v := range vL.([]interface{}) { network := v.(map[string]interface{}) networks[i].label = network["label"].(string) if v, ok := network["ip_address"].(string); ok && v != "" { networks[i].ipv4Address = v } if v, ok := d.GetOk("gateway"); ok { networks[i].ipv4Gateway = v.(string) } if v, ok := network["subnet_mask"].(string); ok && v != "" { ip := net.ParseIP(v).To4() if ip != nil { mask := net.IPv4Mask(ip[0], ip[1], ip[2], ip[3]) pl, _ := mask.Size() networks[i].ipv4PrefixLength = pl } else { return fmt.Errorf("subnet_mask parameter is invalid.") } } if v, ok := network["ipv4_address"].(string); ok && v != "" { networks[i].ipv4Address = v } if v, ok := network["ipv4_prefix_length"].(int); ok && v != 0 { networks[i].ipv4PrefixLength = v } if v, ok := network["ipv4_gateway"].(string); ok && v != "" { networks[i].ipv4Gateway = v } if v, ok := network["ipv6_address"].(string); ok && v != "" { networks[i].ipv6Address = v } if v, ok := network["ipv6_prefix_length"].(int); ok && v != 0 { networks[i].ipv6PrefixLength = v } if v, ok := network["ipv6_gateway"].(string); ok && v != "" { networks[i].ipv6Gateway = v } } vm.networkInterfaces = networks log.Printf("[DEBUG] network_interface init: %v", networks) } if vL, ok := d.GetOk("windows_opt_config"); ok { var winOpt windowsOptConfig custom_configs := (vL.([]interface{}))[0].(map[string]interface{}) if v, ok := custom_configs["admin_password"].(string); ok && v != "" { winOpt.adminPassword = v } if v, ok := custom_configs["domain"].(string); ok && v != "" { winOpt.domain = v } if v, ok := custom_configs["domain_user"].(string); ok && v != "" { winOpt.domainUser = v } if v, ok := custom_configs["product_key"].(string); ok && v != "" { winOpt.productKey = v } if v, ok := custom_configs["domain_user_password"].(string); ok && v != "" { winOpt.domainUserPassword = v } vm.windowsOptionalConfig = winOpt log.Printf("[DEBUG] windows config init: %v", winOpt) } if vL, ok := d.GetOk("disk"); ok { if diskSet, ok := vL.(*schema.Set); ok { disks := []hardDisk{} hasBootableDisk := false for _, value := range diskSet.List() { disk := value.(map[string]interface{}) newDisk := hardDisk{} if v, ok := disk["template"].(string); ok && v != "" { if v, ok := disk["name"].(string); ok && v != "" { return fmt.Errorf("Cannot specify name of a template") } vm.template = v if hasBootableDisk { return fmt.Errorf("[ERROR] Only one bootable disk or template may be given") } hasBootableDisk = true } if v, ok := disk["type"].(string); ok && v != "" { newDisk.initType = v } if v, ok := disk["datastore"].(string); ok && v != "" { vm.datastore = v } if v, ok := disk["size"].(int); ok && v != 0 { if v, ok := disk["template"].(string); ok && v != "" { return fmt.Errorf("Cannot specify size of a template") } if v, ok := disk["name"].(string); ok && v != "" { newDisk.name = v } else { return fmt.Errorf("[ERROR] Disk name must be provided when creating a new disk") } newDisk.size = int64(v) } if v, ok := disk["iops"].(int); ok && v != 0 { newDisk.iops = int64(v) } if v, ok := disk["controller_type"].(string); ok && v != "" { newDisk.controller = v } if vVmdk, ok := disk["vmdk"].(string); ok && vVmdk != "" { if v, ok := disk["template"].(string); ok && v != "" { return fmt.Errorf("Cannot specify a vmdk for a template") } if v, ok := disk["size"].(string); ok && v != "" { return fmt.Errorf("Cannot specify size of a vmdk") } if v, ok := disk["name"].(string); ok && v != "" { return fmt.Errorf("Cannot specify name of a vmdk") } if vBootable, ok := disk["bootable"].(bool); ok { hasBootableDisk = true newDisk.bootable = vBootable vm.hasBootableVmdk = vBootable } newDisk.vmdkPath = vVmdk } // Preserves order so bootable disk is first if newDisk.bootable == true || disk["template"] != "" { disks = append([]hardDisk{newDisk}, disks...) } else { disks = append(disks, newDisk) } } vm.hardDisks = disks log.Printf("[DEBUG] disk init: %v", disks) } } if vL, ok := d.GetOk("cdrom"); ok { cdroms := make([]cdrom, len(vL.([]interface{}))) for i, v := range vL.([]interface{}) { c := v.(map[string]interface{}) if v, ok := c["datastore"].(string); ok && v != "" { cdroms[i].datastore = v } else { return fmt.Errorf("Datastore argument must be specified when attaching a cdrom image.") } if v, ok := c["path"].(string); ok && v != "" { cdroms[i].path = v } else { return fmt.Errorf("Path argument must be specified when attaching a cdrom image.") } } vm.cdroms = cdroms log.Printf("[DEBUG] cdrom init: %v", cdroms) } err := vm.setupVirtualMachine(client) if err != nil { return err } d.SetId(vm.Path()) log.Printf("[INFO] Created virtual machine: %s", d.Id()) return resourceVSphereVirtualMachineRead(d, meta) } func resourceVSphereVirtualMachineRead(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] virtual machine resource data: %#v", d) client := meta.(*govmomi.Client) dc, err := getDatacenter(client, d.Get("datacenter").(string)) if err != nil { return err } finder := find.NewFinder(client.Client, true) finder = finder.SetDatacenter(dc) vm, err := finder.VirtualMachine(context.TODO(), d.Id()) if err != nil { d.SetId("") return nil } var mvm mo.VirtualMachine // wait for interfaces to appear _, err = vm.WaitForNetIP(context.TODO(), true) if err != nil { return err } collector := property.DefaultCollector(client.Client) if err := collector.RetrieveOne(context.TODO(), vm.Reference(), []string{"guest", "summary", "datastore", "config"}, &mvm); err != nil { return err } log.Printf("[DEBUG] Datacenter - %#v", dc) log.Printf("[DEBUG] mvm.Summary.Config - %#v", mvm.Summary.Config) log.Printf("[DEBUG] mvm.Summary.Config - %#v", mvm.Config) log.Printf("[DEBUG] mvm.Guest.Net - %#v", mvm.Guest.Net) disks := make([]map[string]interface{}, 0) templateDisk := make(map[string]interface{}, 1) for _, device := range mvm.Config.Hardware.Device { if vd, ok := device.(*types.VirtualDisk); ok { virtualDevice := vd.GetVirtualDevice() diskFullPath := virtualDevice.Backing.(*types.VirtualDiskFlatVer2BackingInfo).FileName log.Printf("[DEBUG] resourceVSphereVirtualMachineRead - Analyzing disk: %v", diskFullPath) // Separate datastore and path diskFullPathSplit := strings.Split(diskFullPath, " ") if len(diskFullPathSplit) != 2 { return fmt.Errorf("[ERROR] Failed trying to parse disk path: %v", diskFullPath) } diskPath := diskFullPathSplit[1] // Isolate filename diskNameSplit := strings.Split(diskPath, "/") diskName := diskNameSplit[len(diskNameSplit)-1] // Remove possible extension diskName = strings.Split(diskName, ".")[0] if prevDisks, ok := d.GetOk("disk"); ok { if prevDiskSet, ok := prevDisks.(*schema.Set); ok { for _, v := range prevDiskSet.List() { prevDisk := v.(map[string]interface{}) // We're guaranteed only one template disk. Passing value directly through since templates should be immutable if prevDisk["template"] != "" { if len(templateDisk) == 0 { templateDisk = prevDisk disks = append(disks, templateDisk) break } } // It is enforced that prevDisk["name"] should only be set in the case // of creating a new disk for the user. // size case: name was set by user, compare parsed filename from mo.filename (without path or .vmdk extension) with name // vmdk case: compare prevDisk["vmdk"] and mo.Filename if diskName == prevDisk["name"] || diskPath == prevDisk["vmdk"] { prevDisk["key"] = virtualDevice.Key prevDisk["uuid"] = virtualDevice.Backing.(*types.VirtualDiskFlatVer2BackingInfo).Uuid disks = append(disks, prevDisk) break } } } } log.Printf("[DEBUG] disks: %#v", disks) } } err = d.Set("disk", disks) if err != nil { return fmt.Errorf("Invalid disks to set: %#v", disks) } networkInterfaces := make([]map[string]interface{}, 0) for _, v := range mvm.Guest.Net { if v.DeviceConfigId >= 0 { log.Printf("[DEBUG] v.Network - %#v", v.Network) networkInterface := make(map[string]interface{}) networkInterface["label"] = v.Network for _, ip := range v.IpConfig.IpAddress { p := net.ParseIP(ip.IpAddress) if p.To4() != nil { log.Printf("[DEBUG] p.String - %#v", p.String()) log.Printf("[DEBUG] ip.PrefixLength - %#v", ip.PrefixLength) networkInterface["ipv4_address"] = p.String() networkInterface["ipv4_prefix_length"] = ip.PrefixLength } else if p.To16() != nil { log.Printf("[DEBUG] p.String - %#v", p.String()) log.Printf("[DEBUG] ip.PrefixLength - %#v", ip.PrefixLength) networkInterface["ipv6_address"] = p.String() networkInterface["ipv6_prefix_length"] = ip.PrefixLength } log.Printf("[DEBUG] networkInterface: %#v", networkInterface) } log.Printf("[DEBUG] networkInterface: %#v", networkInterface) networkInterfaces = append(networkInterfaces, networkInterface) } } if mvm.Guest.IpStack != nil { for _, v := range mvm.Guest.IpStack { if v.IpRouteConfig != nil && v.IpRouteConfig.IpRoute != nil { for _, route := range v.IpRouteConfig.IpRoute { if route.Gateway.Device != "" { gatewaySetting := "" if route.Network == "::" { gatewaySetting = "ipv6_gateway" } else if route.Network == "0.0.0.0" { gatewaySetting = "ipv4_gateway" } if gatewaySetting != "" { deviceID, err := strconv.Atoi(route.Gateway.Device) if err != nil { log.Printf("[WARN] error at processing %s of device id %#v: %#v", gatewaySetting, route.Gateway.Device, err) } else { log.Printf("[DEBUG] %s of device id %d: %s", gatewaySetting, deviceID, route.Gateway.IpAddress) networkInterfaces[deviceID][gatewaySetting] = route.Gateway.IpAddress } } } } } } } log.Printf("[DEBUG] networkInterfaces: %#v", networkInterfaces) err = d.Set("network_interface", networkInterfaces) if err != nil { return fmt.Errorf("Invalid network interfaces to set: %#v", networkInterfaces) } log.Printf("[DEBUG] ip address: %v", networkInterfaces[0]["ipv4_address"].(string)) d.SetConnInfo(map[string]string{ "type": "ssh", "host": networkInterfaces[0]["ipv4_address"].(string), }) var rootDatastore string for _, v := range mvm.Datastore { var md mo.Datastore if err := collector.RetrieveOne(context.TODO(), v, []string{"name", "parent"}, &md); err != nil { return err } if md.Parent.Type == "StoragePod" { var msp mo.StoragePod if err := collector.RetrieveOne(context.TODO(), *md.Parent, []string{"name"}, &msp); err != nil { return err } rootDatastore = msp.Name log.Printf("[DEBUG] %#v", msp.Name) } else { rootDatastore = md.Name log.Printf("[DEBUG] %#v", md.Name) } break } d.Set("datacenter", dc) d.Set("memory", mvm.Summary.Config.MemorySizeMB) d.Set("memory_reservation", mvm.Summary.Config.MemoryReservation) d.Set("cpu", mvm.Summary.Config.NumCpu) d.Set("datastore", rootDatastore) return nil } func resourceVSphereVirtualMachineDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*govmomi.Client) dc, err := getDatacenter(client, d.Get("datacenter").(string)) if err != nil { return err } finder := find.NewFinder(client.Client, true) finder = finder.SetDatacenter(dc) vm, err := finder.VirtualMachine(context.TODO(), vmPath(d.Get("folder").(string), d.Get("name").(string))) if err != nil { return err } log.Printf("[INFO] Deleting virtual machine: %s", d.Id()) state, err := vm.PowerState(context.TODO()) if err != nil { return err } if state == types.VirtualMachinePowerStatePoweredOn { task, err := vm.PowerOff(context.TODO()) if err != nil { return err } err = task.Wait(context.TODO()) if err != nil { return err } } task, err := vm.Destroy(context.TODO()) if err != nil { return err } err = task.Wait(context.TODO()) if err != nil { return err } d.SetId("") return nil } // addHardDisk adds a new Hard Disk to the VirtualMachine. func addHardDisk(vm *object.VirtualMachine, size, iops int64, diskType string, datastore *object.Datastore, diskPath string, controller_type string) error { devices, err := vm.Device(context.TODO()) if err != nil { return err } log.Printf("[DEBUG] vm devices: %#v\n", devices) controller, err := devices.FindDiskController(controller_type) if err != nil { return err } log.Printf("[DEBUG] disk controller: %#v\n", controller) // TODO Check if diskPath & datastore exist // If diskPath is not specified, pass empty string to CreateDisk() if diskPath == "" { return fmt.Errorf("[ERROR] addHardDisk - No path proided") } else { // TODO Check if diskPath & datastore exist diskPath = fmt.Sprintf("[%v] %v", datastore.Name(), diskPath) } log.Printf("[DEBUG] addHardDisk - diskPath: %v", diskPath) disk := devices.CreateDisk(controller, datastore.Reference(), diskPath) existing := devices.SelectByBackingInfo(disk.Backing) log.Printf("[DEBUG] disk: %#v\n", disk) if len(existing) == 0 { disk.CapacityInKB = int64(size * 1024 * 1024) if iops != 0 { disk.StorageIOAllocation = &types.StorageIOAllocationInfo{ Limit: iops, } } backing := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo) if diskType == "eager_zeroed" { // eager zeroed thick virtual disk backing.ThinProvisioned = types.NewBool(false) backing.EagerlyScrub = types.NewBool(true) } else if diskType == "thin" { // thin provisioned virtual disk backing.ThinProvisioned = types.NewBool(true) } log.Printf("[DEBUG] addHardDisk: %#v\n", disk) log.Printf("[DEBUG] addHardDisk capacity: %#v\n", disk.CapacityInKB) return vm.AddDevice(context.TODO(), disk) } else { log.Printf("[DEBUG] addHardDisk: Disk already present.\n") return nil } } // addCdrom adds a new virtual cdrom drive to the VirtualMachine and attaches an image (ISO) to it from a datastore path. func addCdrom(vm *object.VirtualMachine, datastore, path string) error { devices, err := vm.Device(context.TODO()) if err != nil { return err } log.Printf("[DEBUG] vm devices: %#v", devices) controller, err := devices.FindIDEController("") if err != nil { return err } log.Printf("[DEBUG] ide controller: %#v", controller) c, err := devices.CreateCdrom(controller) if err != nil { return err } c = devices.InsertIso(c, fmt.Sprintf("[%s] %s", datastore, path)) log.Printf("[DEBUG] addCdrom: %#v", c) return vm.AddDevice(context.TODO(), c) } // buildNetworkDevice builds VirtualDeviceConfigSpec for Network Device. func buildNetworkDevice(f *find.Finder, label, adapterType string) (*types.VirtualDeviceConfigSpec, error) { network, err := f.Network(context.TODO(), "*"+label) if err != nil { return nil, err } backing, err := network.EthernetCardBackingInfo(context.TODO()) if err != nil { return nil, err } if adapterType == "vmxnet3" { return &types.VirtualDeviceConfigSpec{ Operation: types.VirtualDeviceConfigSpecOperationAdd, Device: &types.VirtualVmxnet3{ VirtualVmxnet: types.VirtualVmxnet{ VirtualEthernetCard: types.VirtualEthernetCard{ VirtualDevice: types.VirtualDevice{ Key: -1, Backing: backing, }, AddressType: string(types.VirtualEthernetCardMacTypeGenerated), }, }, }, }, nil } else if adapterType == "e1000" { return &types.VirtualDeviceConfigSpec{ Operation: types.VirtualDeviceConfigSpecOperationAdd, Device: &types.VirtualE1000{ VirtualEthernetCard: types.VirtualEthernetCard{ VirtualDevice: types.VirtualDevice{ Key: -1, Backing: backing, }, AddressType: string(types.VirtualEthernetCardMacTypeGenerated), }, }, }, nil } else { return nil, fmt.Errorf("Invalid network adapter type.") } } // buildVMRelocateSpec builds VirtualMachineRelocateSpec to set a place for a new VirtualMachine. func buildVMRelocateSpec(rp *object.ResourcePool, ds *object.Datastore, vm *object.VirtualMachine, linkedClone bool, initType string) (types.VirtualMachineRelocateSpec, error) { var key int32 var moveType string if linkedClone { moveType = "createNewChildDiskBacking" } else { moveType = "moveAllDiskBackingsAndDisallowSharing" } log.Printf("[DEBUG] relocate type: [%s]", moveType) devices, err := vm.Device(context.TODO()) if err != nil { return types.VirtualMachineRelocateSpec{}, err } for _, d := range devices { if devices.Type(d) == "disk" { key = int32(d.GetVirtualDevice().Key) } } isThin := initType == "thin" rpr := rp.Reference() dsr := ds.Reference() return types.VirtualMachineRelocateSpec{ Datastore: &dsr, Pool: &rpr, DiskMoveType: moveType, Disk: []types.VirtualMachineRelocateSpecDiskLocator{ { Datastore: dsr, DiskBackingInfo: &types.VirtualDiskFlatVer2BackingInfo{ DiskMode: "persistent", ThinProvisioned: types.NewBool(isThin), EagerlyScrub: types.NewBool(!isThin), }, DiskId: key, }, }, }, nil } // getDatastoreObject gets datastore object. func getDatastoreObject(client *govmomi.Client, f *object.DatacenterFolders, name string) (types.ManagedObjectReference, error) { s := object.NewSearchIndex(client.Client) ref, err := s.FindChild(context.TODO(), f.DatastoreFolder, name) if err != nil { return types.ManagedObjectReference{}, err } if ref == nil { return types.ManagedObjectReference{}, fmt.Errorf("Datastore '%s' not found.", name) } log.Printf("[DEBUG] getDatastoreObject: reference: %#v", ref) return ref.Reference(), nil } // buildStoragePlacementSpecCreate builds StoragePlacementSpec for create action. func buildStoragePlacementSpecCreate(f *object.DatacenterFolders, rp *object.ResourcePool, storagePod object.StoragePod, configSpec types.VirtualMachineConfigSpec) types.StoragePlacementSpec { vmfr := f.VmFolder.Reference() rpr := rp.Reference() spr := storagePod.Reference() sps := types.StoragePlacementSpec{ Type: "create", ConfigSpec: &configSpec, PodSelectionSpec: types.StorageDrsPodSelectionSpec{ StoragePod: &spr, }, Folder: &vmfr, ResourcePool: &rpr, } log.Printf("[DEBUG] findDatastore: StoragePlacementSpec: %#v\n", sps) return sps } // buildStoragePlacementSpecClone builds StoragePlacementSpec for clone action. func buildStoragePlacementSpecClone(c *govmomi.Client, f *object.DatacenterFolders, vm *object.VirtualMachine, rp *object.ResourcePool, storagePod object.StoragePod) types.StoragePlacementSpec { vmr := vm.Reference() vmfr := f.VmFolder.Reference() rpr := rp.Reference() spr := storagePod.Reference() var o mo.VirtualMachine err := vm.Properties(context.TODO(), vmr, []string{"datastore"}, &o) if err != nil { return types.StoragePlacementSpec{} } ds := object.NewDatastore(c.Client, o.Datastore[0]) log.Printf("[DEBUG] findDatastore: datastore: %#v\n", ds) devices, err := vm.Device(context.TODO()) if err != nil { return types.StoragePlacementSpec{} } var key int32 for _, d := range devices.SelectByType((*types.VirtualDisk)(nil)) { key = int32(d.GetVirtualDevice().Key) log.Printf("[DEBUG] findDatastore: virtual devices: %#v\n", d.GetVirtualDevice()) } sps := types.StoragePlacementSpec{ Type: "clone", Vm: &vmr, PodSelectionSpec: types.StorageDrsPodSelectionSpec{ StoragePod: &spr, }, CloneSpec: &types.VirtualMachineCloneSpec{ Location: types.VirtualMachineRelocateSpec{ Disk: []types.VirtualMachineRelocateSpecDiskLocator{ { Datastore: ds.Reference(), DiskBackingInfo: &types.VirtualDiskFlatVer2BackingInfo{}, DiskId: key, }, }, Pool: &rpr, }, PowerOn: false, Template: false, }, CloneName: "dummy", Folder: &vmfr, } return sps } // findDatastore finds Datastore object. func findDatastore(c *govmomi.Client, sps types.StoragePlacementSpec) (*object.Datastore, error) { var datastore *object.Datastore log.Printf("[DEBUG] findDatastore: StoragePlacementSpec: %#v\n", sps) srm := object.NewStorageResourceManager(c.Client) rds, err := srm.RecommendDatastores(context.TODO(), sps) if err != nil { return nil, err } log.Printf("[DEBUG] findDatastore: recommendDatastores: %#v\n", rds) spa := rds.Recommendations[0].Action[0].(*types.StoragePlacementAction) datastore = object.NewDatastore(c.Client, spa.Destination) log.Printf("[DEBUG] findDatastore: datastore: %#v", datastore) return datastore, nil } // createCdroms is a helper function to attach virtual cdrom devices (and their attached disk images) to a virtual IDE controller. func createCdroms(vm *object.VirtualMachine, cdroms []cdrom) error { log.Printf("[DEBUG] add cdroms: %v", cdroms) for _, cd := range cdroms { log.Printf("[DEBUG] add cdrom (datastore): %v", cd.datastore) log.Printf("[DEBUG] add cdrom (cd path): %v", cd.path) err := addCdrom(vm, cd.datastore, cd.path) if err != nil { return err } } return nil } func (vm *virtualMachine) setupVirtualMachine(c *govmomi.Client) error { dc, err := getDatacenter(c, vm.datacenter) if err != nil { return err } finder := find.NewFinder(c.Client, true) finder = finder.SetDatacenter(dc) var template *object.VirtualMachine var template_mo mo.VirtualMachine var vm_mo mo.VirtualMachine if vm.template != "" { template, err = finder.VirtualMachine(context.TODO(), vm.template) if err != nil { return err } log.Printf("[DEBUG] template: %#v", template) err = template.Properties(context.TODO(), template.Reference(), []string{"parent", "config.template", "config.guestId", "resourcePool", "snapshot", "guest.toolsVersionStatus2", "config.guestFullName"}, &template_mo) if err != nil { return err } } var resourcePool *object.ResourcePool if vm.resourcePool == "" { if vm.cluster == "" { resourcePool, err = finder.DefaultResourcePool(context.TODO()) if err != nil { return err } } else { resourcePool, err = finder.ResourcePool(context.TODO(), "*"+vm.cluster+"/Resources") if err != nil { return err } } } else { resourcePool, err = finder.ResourcePool(context.TODO(), vm.resourcePool) if err != nil { return err } } log.Printf("[DEBUG] resource pool: %#v", resourcePool) dcFolders, err := dc.Folders(context.TODO()) if err != nil { return err } log.Printf("[DEBUG] folder: %#v", vm.folder) folder := dcFolders.VmFolder if len(vm.folder) > 0 { si := object.NewSearchIndex(c.Client) folderRef, err := si.FindByInventoryPath( context.TODO(), fmt.Sprintf("%v/vm/%v", vm.datacenter, vm.folder)) if err != nil { return fmt.Errorf("Error reading folder %s: %s", vm.folder, err) } else if folderRef == nil { return fmt.Errorf("Cannot find folder %s", vm.folder) } else { folder = folderRef.(*object.Folder) } } // make config spec configSpec := types.VirtualMachineConfigSpec{ Name: vm.name, NumCPUs: vm.vcpu, NumCoresPerSocket: 1, MemoryMB: vm.memoryMb, MemoryAllocation: &types.ResourceAllocationInfo{ Reservation: vm.memoryAllocation.reservation, }, } if vm.template == "" { configSpec.GuestId = "otherLinux64Guest" } log.Printf("[DEBUG] virtual machine config spec: %v", configSpec) // make ExtraConfig log.Printf("[DEBUG] virtual machine Extra Config spec start") if len(vm.customConfigurations) > 0 { var ov []types.BaseOptionValue for k, v := range vm.customConfigurations { key := k value := v o := types.OptionValue{ Key: key, Value: &value, } log.Printf("[DEBUG] virtual machine Extra Config spec: %s,%s", k, v) ov = append(ov, &o) } configSpec.ExtraConfig = ov log.Printf("[DEBUG] virtual machine Extra Config spec: %v", configSpec.ExtraConfig) } var datastore *object.Datastore if vm.datastore == "" { datastore, err = finder.DefaultDatastore(context.TODO()) if err != nil { return err } } else { datastore, err = finder.Datastore(context.TODO(), vm.datastore) if err != nil { // TODO: datastore cluster support in govmomi finder function d, err := getDatastoreObject(c, dcFolders, vm.datastore) if err != nil { return err } if d.Type == "StoragePod" { sp := object.StoragePod{ Folder: object.NewFolder(c.Client, d), } var sps types.StoragePlacementSpec if vm.template != "" { sps = buildStoragePlacementSpecClone(c, dcFolders, template, resourcePool, sp) } else { sps = buildStoragePlacementSpecCreate(dcFolders, resourcePool, sp, configSpec) } datastore, err = findDatastore(c, sps) if err != nil { return err } } else { datastore = object.NewDatastore(c.Client, d) } } } log.Printf("[DEBUG] datastore: %#v", datastore) // network networkDevices := []types.BaseVirtualDeviceConfigSpec{} networkConfigs := []types.CustomizationAdapterMapping{} for _, network := range vm.networkInterfaces { // network device var networkDeviceType string if vm.template == "" { networkDeviceType = "e1000" } else { networkDeviceType = "vmxnet3" } nd, err := buildNetworkDevice(finder, network.label, networkDeviceType) if err != nil { return err } networkDevices = append(networkDevices, nd) if vm.template != "" { var ipSetting types.CustomizationIPSettings if network.ipv4Address == "" { ipSetting.Ip = &types.CustomizationDhcpIpGenerator{} } else { if network.ipv4PrefixLength == 0 { return fmt.Errorf("Error: ipv4_prefix_length argument is empty.") } m := net.CIDRMask(network.ipv4PrefixLength, 32) sm := net.IPv4(m[0], m[1], m[2], m[3]) subnetMask := sm.String() log.Printf("[DEBUG] ipv4 gateway: %v\n", network.ipv4Gateway) log.Printf("[DEBUG] ipv4 address: %v\n", network.ipv4Address) log.Printf("[DEBUG] ipv4 prefix length: %v\n", network.ipv4PrefixLength) log.Printf("[DEBUG] ipv4 subnet mask: %v\n", subnetMask) ipSetting.Gateway = []string{ network.ipv4Gateway, } ipSetting.Ip = &types.CustomizationFixedIp{ IpAddress: network.ipv4Address, } ipSetting.SubnetMask = subnetMask } ipv6Spec := &types.CustomizationIPSettingsIpV6AddressSpec{} if network.ipv6Address == "" { ipv6Spec.Ip = []types.BaseCustomizationIpV6Generator{ &types.CustomizationDhcpIpV6Generator{}, } } else { log.Printf("[DEBUG] ipv6 gateway: %v\n", network.ipv6Gateway) log.Printf("[DEBUG] ipv6 address: %v\n", network.ipv6Address) log.Printf("[DEBUG] ipv6 prefix length: %v\n", network.ipv6PrefixLength) ipv6Spec.Ip = []types.BaseCustomizationIpV6Generator{ &types.CustomizationFixedIpV6{ IpAddress: network.ipv6Address, SubnetMask: int32(network.ipv6PrefixLength), }, } ipv6Spec.Gateway = []string{network.ipv6Gateway} } ipSetting.IpV6Spec = ipv6Spec // network config config := types.CustomizationAdapterMapping{ Adapter: ipSetting, } networkConfigs = append(networkConfigs, config) } } log.Printf("[DEBUG] network devices: %v", networkDevices) log.Printf("[DEBUG] network configs: %v", networkConfigs) var task *object.Task if vm.template == "" { var mds mo.Datastore if err = datastore.Properties(context.TODO(), datastore.Reference(), []string{"name"}, &mds); err != nil { return err } log.Printf("[DEBUG] datastore: %#v", mds.Name) scsi, err := object.SCSIControllerTypes().CreateSCSIController("scsi") if err != nil { log.Printf("[ERROR] %s", err) } configSpec.DeviceChange = append(configSpec.DeviceChange, &types.VirtualDeviceConfigSpec{ Operation: types.VirtualDeviceConfigSpecOperationAdd, Device: scsi, }) configSpec.Files = &types.VirtualMachineFileInfo{VmPathName: fmt.Sprintf("[%s]", mds.Name)} task, err = folder.CreateVM(context.TODO(), configSpec, resourcePool, nil) if err != nil { log.Printf("[ERROR] %s", err) } err = task.Wait(context.TODO()) if err != nil { log.Printf("[ERROR] %s", err) } } else { relocateSpec, err := buildVMRelocateSpec(resourcePool, datastore, template, vm.linkedClone, vm.hardDisks[0].initType) if err != nil { return err } log.Printf("[DEBUG] relocate spec: %v", relocateSpec) // make vm clone spec cloneSpec := types.VirtualMachineCloneSpec{ Location: relocateSpec, Template: false, Config: &configSpec, PowerOn: false, } if vm.linkedClone { if template_mo.Snapshot == nil { return fmt.Errorf("`linkedClone=true`, but image VM has no snapshots") } cloneSpec.Snapshot = template_mo.Snapshot.CurrentSnapshot } log.Printf("[DEBUG] clone spec: %v", cloneSpec) task, err = template.Clone(context.TODO(), folder, vm.name, cloneSpec) if err != nil { return err } } err = task.Wait(context.TODO()) if err != nil { log.Printf("[ERROR] %s", err) } newVM, err := finder.VirtualMachine(context.TODO(), vm.Path()) if err != nil { return err } log.Printf("[DEBUG] new vm: %v", newVM) devices, err := newVM.Device(context.TODO()) if err != nil { log.Printf("[DEBUG] Template devices can't be found") return err } for _, dvc := range devices { // Issue 3559/3560: Delete all ethernet devices to add the correct ones later if devices.Type(dvc) == "ethernet" { err := newVM.RemoveDevice(context.TODO(), false, dvc) if err != nil { return err } } } // Add Network devices for _, dvc := range networkDevices { err := newVM.AddDevice( context.TODO(), dvc.GetVirtualDeviceConfigSpec().Device) if err != nil { return err } } // Create the cdroms if needed. if err := createCdroms(newVM, vm.cdroms); err != nil { return err } newVM.Properties(context.TODO(), newVM.Reference(), []string{"summary", "config"}, &vm_mo) firstDisk := 0 if vm.template != "" { firstDisk++ } for i := firstDisk; i < len(vm.hardDisks); i++ { log.Printf("[DEBUG] disk index: %v", i) var diskPath string switch { case vm.hardDisks[i].vmdkPath != "": diskPath = vm.hardDisks[i].vmdkPath case vm.hardDisks[i].name != "": snapshotFullDir := vm_mo.Config.Files.SnapshotDirectory split := strings.Split(snapshotFullDir, " ") if len(split) != 2 { return fmt.Errorf("[ERROR] setupVirtualMachine - failed to split snapshot directory: %v", snapshotFullDir) } vmWorkingPath := split[1] diskPath = vmWorkingPath + vm.hardDisks[i].name default: return fmt.Errorf("[ERROR] setupVirtualMachine - Neither vmdk path nor vmdk name was given: %#v", vm.hardDisks[i]) } err = addHardDisk(newVM, vm.hardDisks[i].size, vm.hardDisks[i].iops, vm.hardDisks[i].initType, datastore, diskPath, vm.hardDisks[i].controller) if err != nil { return err } } if vm.skipCustomization || vm.template == "" { log.Printf("[DEBUG] VM customization skipped") } else { var identity_options types.BaseCustomizationIdentitySettings if strings.HasPrefix(template_mo.Config.GuestId, "win") { var timeZone int if vm.timeZone == "Etc/UTC" { vm.timeZone = "085" } timeZone, err := strconv.Atoi(vm.timeZone) if err != nil { return fmt.Errorf("Error converting TimeZone: %s", err) } guiUnattended := types.CustomizationGuiUnattended{ AutoLogon: false, AutoLogonCount: 1, TimeZone: int32(timeZone), } customIdentification := types.CustomizationIdentification{} userData := types.CustomizationUserData{ ComputerName: &types.CustomizationFixedName{ Name: strings.Split(vm.name, ".")[0], }, ProductId: vm.windowsOptionalConfig.productKey, FullName: "terraform", OrgName: "terraform", } if vm.windowsOptionalConfig.domainUserPassword != "" && vm.windowsOptionalConfig.domainUser != "" && vm.windowsOptionalConfig.domain != "" { customIdentification.DomainAdminPassword = &types.CustomizationPassword{ PlainText: true, Value: vm.windowsOptionalConfig.domainUserPassword, } customIdentification.DomainAdmin = vm.windowsOptionalConfig.domainUser customIdentification.JoinDomain = vm.windowsOptionalConfig.domain } if vm.windowsOptionalConfig.adminPassword != "" { guiUnattended.Password = &types.CustomizationPassword{ PlainText: true, Value: vm.windowsOptionalConfig.adminPassword, } } identity_options = &types.CustomizationSysprep{ GuiUnattended: guiUnattended, Identification: customIdentification, UserData: userData, } } else { identity_options = &types.CustomizationLinuxPrep{ HostName: &types.CustomizationFixedName{ Name: strings.Split(vm.name, ".")[0], }, Domain: vm.domain, TimeZone: vm.timeZone, HwClockUTC: types.NewBool(true), } } // create CustomizationSpec customSpec := types.CustomizationSpec{ Identity: identity_options, GlobalIPSettings: types.CustomizationGlobalIPSettings{ DnsSuffixList: vm.dnsSuffixes, DnsServerList: vm.dnsServers, }, NicSettingMap: networkConfigs, } log.Printf("[DEBUG] custom spec: %v", customSpec) log.Printf("[DEBUG] VM customization starting") taskb, err := newVM.Customize(context.TODO(), customSpec) if err != nil { return err } _, err = taskb.WaitForResult(context.TODO(), nil) if err != nil { return err } log.Printf("[DEBUG] VM customization finished") } if vm.hasBootableVmdk || vm.template != "" { newVM.PowerOn(context.TODO()) } return nil }