diff --git a/tf-vsphere-devrc.mk.example b/tf-vsphere-devrc.mk.example index 25db24d84..af8dff86e 100644 --- a/tf-vsphere-devrc.mk.example +++ b/tf-vsphere-devrc.mk.example @@ -52,6 +52,7 @@ export TF_VAR_VSPHERE_ADAPTER_TYPE = lsiLogic export TF_VAR_VSPHERE_DC_FOLDER = dc-folder export TF_VAR_VSPHERE_DS_FOLDER = datastore-folder/foo export TF_VAR_VSPHERE_USE_LINKED_CLONE = true +export TF_VAR_VSPHERE_USE_INSTANT_CLONE = true export TF_VAR_VSPHERE_PERSIST_SESSION = true export TF_VAR_VSPHERE_CLONED_VM_DISK_SIZE = 32 export TF_VAR_VSPHERE_CONTENT_LIBRARY_FILES = '[ "https://acctest-images.storage.googleapis.com/template_test.ovf", "https://acctest-images.storage.googleapis.com/template_test.mf", "https://acctest-images.storage.googleapis.com/template_test-1.vmdk" ]' diff --git a/vsphere/internal/helper/storagepod/storage_pod_helper.go b/vsphere/internal/helper/storagepod/storage_pod_helper.go index 2fe8ba8b7..ba63495d7 100644 --- a/vsphere/internal/helper/storagepod/storage_pod_helper.go +++ b/vsphere/internal/helper/storagepod/storage_pod_helper.go @@ -15,6 +15,7 @@ import ( "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" ) @@ -518,3 +519,56 @@ func IsMember(pod *object.StoragePod, ds *object.Datastore) (bool, error) { } return true, nil } + +// get_recommended_datastore from storagePod +func Get_recommended_Datastore(client *govmomi.Client, + fo *object.Folder, + name string, + timeout int, + pod *object.StoragePod, +) (*object.Datastore, error) { + sdrsEnabled, err := StorageDRSEnabled(pod) + if err != nil { + return nil, err + } + if !sdrsEnabled { + return nil, fmt.Errorf("storage DRS is not enabled on datastore cluster %q", pod.Name()) + } + log.Printf( + "[DEBUG] Instant Cloning virtual machine to %q on datastore cluster %q", + fmt.Sprintf("%s/%s", fo.InventoryPath, name), + pod.Name(), + ) + sps := types.StoragePlacementSpec{ + Type: string(types.StoragePlacementSpecPlacementTypeCreate), + PodSelectionSpec: types.StorageDrsPodSelectionSpec{ + StoragePod: types.NewReference(pod.Reference()), + }, + } + log.Printf("[DEBUG] Acquiring Storage DRS recommendations (type: %q)", sps.Type) + srm := object.NewStorageResourceManager(client.Client) + ctx, cancel := context.WithTimeout(context.Background(), 100) + defer cancel() + + placement, err := srm.RecommendDatastores(ctx, sps) + if err != nil { + return nil, err + } + recs := placement.Recommendations + if len(recs) < 1 { + return nil, fmt.Errorf("no storage DRS recommendations were found for the requested action (type: %q)", sps.Type) + } + // result to pin disks to recommended datastores + ds := recs[0].Action[0].(*types.StoragePlacementAction).Destination + + var mds mo.Datastore + err = property.DefaultCollector(client.Client).RetrieveOne(ctx, ds, []string{"name"}, &mds) + if err != nil { + return nil, err + } + + datastore := object.NewDatastore(client.Client, ds) + datastore.InventoryPath = mds.Name + return datastore, nil + +} diff --git a/vsphere/internal/helper/virtualmachine/virtual_machine_helper.go b/vsphere/internal/helper/virtualmachine/virtual_machine_helper.go index 65353db97..7908accba 100644 --- a/vsphere/internal/helper/virtualmachine/virtual_machine_helper.go +++ b/vsphere/internal/helper/virtualmachine/virtual_machine_helper.go @@ -543,6 +543,30 @@ func Clone(c *govmomi.Client, src *object.VirtualMachine, f *object.Folder, name return FromMOID(c, result.Result.(types.ManagedObjectReference).Value) } +// InstantClone wraps the creation of a virtual machine instantly and the subsequent waiting of +// the task. A higher-level virtual machine object is returned. +func InstantClone(c *govmomi.Client, src *object.VirtualMachine, f *object.Folder, name string, spec types.VirtualMachineInstantCloneSpec, timeout int) (*object.VirtualMachine, error) { + log.Printf("[DEBUG] Instant Cloning virtual machine %q", fmt.Sprintf("%s/%s", f.InventoryPath, name)) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(timeout)) + defer cancel() + task, err := src.InstantClone(ctx, spec) + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + err = errors.New("timeout waiting for Instant clone to complete") + } + return nil, err + } + result, err := task.WaitForResult(ctx, nil) + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + err = errors.New("timeout waiting for Instant clone to complete") + } + return nil, err + } + log.Printf("[DEBUG] Virtual machine %q: Instant clone complete (MOID: %q)", fmt.Sprintf("%s/%s", f.InventoryPath, name), result.Result.(types.ManagedObjectReference).Value) + return FromMOID(c, result.Result.(types.ManagedObjectReference).Value) +} + // Deploy clones a virtual machine from a content library item. func Deploy(deployData *VCenterDeploy) (*types.ManagedObjectReference, error) { log.Printf("[DEBUG] virtualmachine.Deploy: Deploying VM from Content Library item.") diff --git a/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go b/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go index 94c71f614..21eabc53f 100644 --- a/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go +++ b/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go @@ -7,8 +7,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/datastore" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/folder" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/hostsystem" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/resourcepool" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/storagepod" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/virtualmachine" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/virtualdevice" "github.com/vmware/govmomi" @@ -34,6 +36,11 @@ func VirtualMachineCloneSchema() map[string]*schema.Schema { Optional: true, Description: "Whether or not to create a linked clone when cloning. When this option is used, the source VM must have a single snapshot associated with it.", }, + "instant_clone": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether or not to create a instant clone when cloning. When this option is used, the source VM must be in a running state.", + }, "timeout": { Type: schema.TypeInt, Optional: true, @@ -255,3 +262,70 @@ func ExpandVirtualMachineCloneSpec(d *schema.ResourceData, c *govmomi.Client) (t log.Printf("[DEBUG] ExpandVirtualMachineCloneSpec: Clone spec prep complete") return spec, vm, nil } + +// ExpandVirtualMachineInstantCloneSpec creates an Instant clone spec for an existing virtual machine. +// +// The clone spec built by this function for the clone contains the target +// datastore, the source snapshot in the event of linked clones, and a relocate +// spec that contains the new locations and configuration details of the new +// virtual disks. +func ExpandVirtualMachineInstantCloneSpec(d *schema.ResourceData, client *govmomi.Client) (types.VirtualMachineInstantCloneSpec, *object.VirtualMachine, error) { + + var spec types.VirtualMachineInstantCloneSpec + log.Printf("[DEBUG] ExpandVirtualMachineInstantCloneSpec: Preparing InstantClone spec for VM") + + //find parent vm + tUUID := d.Get("clone.0.template_uuid").(string) // uuid moid or parent VM name + log.Printf("[DEBUG] ExpandVirtualMachineInstantCloneSpec: Instant Cloning from UUID: %s", tUUID) + vm, err := virtualmachine.FromUUID(client, tUUID) + if err != nil { + return spec, nil, fmt.Errorf("cannot locate virtual machine with UUID %q: %s", tUUID, err) + } + // Populate the datastore only if we have a datastore ID. The ID may not be + // specified in the event a datastore cluster is specified instead. + if dsID, ok := d.GetOk("datastore_id"); ok { + ds, err := datastore.FromID(client, dsID.(string)) + if err != nil { + return spec, nil, fmt.Errorf("error locating datastore for VM: %s", err) + } + spec.Location.Datastore = types.NewReference(ds.Reference()) + } + // Set the target resource pool. + poolID := d.Get("resource_pool_id").(string) + pool, err := resourcepool.FromID(client, poolID) + if err != nil { + return spec, nil, fmt.Errorf("could not find resource pool ID %q: %s", poolID, err) + } + poolRef := pool.Reference() + spec.Location.Pool = &poolRef + + // set the folder // when folder specified + fo, err := folder.VirtualMachineFolderFromObject(client, pool, d.Get("folder").(string)) + if err != nil { + return spec, nil, err + } + folderRef := fo.Reference() + spec.Location.Folder = &folderRef + + // else if + // datastore cluster + var ds *object.Datastore + if _, ok := d.GetOk("datastore_cluster_id"); ok { + pod, err := storagepod.FromID(client, d.Get("datastore_cluster_id").(string)) + if err != nil { + return spec, nil, fmt.Errorf("error getting datastore cluster: %s", err) + } + if pod != nil { + ds, err = storagepod.Get_recommended_Datastore(client, fo, d.Get("datastore_cluster_id").(string), 100, pod) + if err != nil { + return spec, nil, err + } + spec.Location.Datastore = types.NewReference(ds.Reference()) + } + } + // set the name + spec.Name = d.Get("name").(string) + + log.Printf("[DEBUG] ExpandVirtualMachineInstantCloneSpec: Instant Clone spec prep complete") + return spec, vm, nil +} diff --git a/vsphere/resource_vsphere_virtual_machine.go b/vsphere/resource_vsphere_virtual_machine.go index fcf2aa6cc..486d5e8bc 100644 --- a/vsphere/resource_vsphere_virtual_machine.go +++ b/vsphere/resource_vsphere_virtual_machine.go @@ -518,6 +518,7 @@ func resourceVSphereVirtualMachineRead(d *schema.ResourceData, meta interface{}) // Read the state of the SCSI bus. _ = d.Set("scsi_type", virtualdevice.ReadSCSIBusType(devices, d.Get("scsi_controller_count").(int))) _ = d.Set("scsi_bus_sharing", virtualdevice.ReadSCSIBusSharing(devices, d.Get("scsi_controller_count").(int))) + // Disks first if err := virtualdevice.DiskRefreshOperation(d, client, devices); err != nil { return err @@ -1450,6 +1451,22 @@ func resourceVSphereVirtualMachineCreateClone(d *schema.ResourceData, meta inter name := d.Get("name").(string) timeout := d.Get("clone.0.timeout").(int) var vm *object.VirtualMachine + + // instant Clone + if d.Get("clone.0.instant_clone").(bool) { + log.Printf("[DEBUG] %s: Instant Clone being created from VM", resourceVSphereVirtualMachineIDString(d)) + // Expand the clone spec. We get the source VM here too. + cloneSpec, srcVM, err := vmworkflow.ExpandVirtualMachineInstantCloneSpec(d, client) + if err != nil { + return nil, err + } + vm, err = virtualmachine.InstantClone(client, srcVM, fo, name, cloneSpec, timeout) + if err != nil { + return nil, fmt.Errorf("error Instant cloning virtual machine: %s", err) + } + return vm, resourceVSphereVirtualMachinePostDeployChanges(d, meta, vm, false) + } + switch contentlibrary.IsContentLibraryItem(meta.(*Client).restClient, d.Get("clone.0.template_uuid").(string)) { case true: deploySpec, err := createVCenterDeploy(d, meta) @@ -1630,10 +1647,13 @@ func resourceVSphereVirtualMachinePostDeployChanges(d *schema.ResourceData, meta return fmt.Errorf("error sending customization spec: %s", err) } } + // Finally time to power on the virtual machine! pTimeout := time.Duration(d.Get("poweron_timeout").(int)) * time.Second - if err := virtualmachine.PowerOn(vm, pTimeout); err != nil { - return fmt.Errorf("error powering on virtual machine: %s", err) + if vprops.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOff { + if err := virtualmachine.PowerOn(vm, pTimeout); err != nil { + return fmt.Errorf("error powering on virtual machine: %s", err) + } } // If we customized, wait on customization. if cw != nil { diff --git a/vsphere/resource_vsphere_virtual_machine_test.go b/vsphere/resource_vsphere_virtual_machine_test.go index c835764a3..3e7014cbd 100644 --- a/vsphere/resource_vsphere_virtual_machine_test.go +++ b/vsphere/resource_vsphere_virtual_machine_test.go @@ -1760,6 +1760,24 @@ func TestAccResourceVSphereVirtualMachine_cloneWithBadSizeWithLinkedClone(t *tes }) } +func TestAccResourceVSphereVirtualMachine_Instantclone(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccResourceVSphereVirtualMachinePreInstantCloneCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccResourceVSphereVirtualMachineCheckExists(false), + Steps: []resource.TestStep{ + { + Config: testAccResourceVSphereVirtualMachineConfigInstant(), + Check: testAccResourceVSphereVirtualMachineCheckExists(true), + }, + }, + }) +} + func TestAccResourceVSphereVirtualMachine_cloneWithBadSizeWithoutLinkedClone(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { @@ -2529,7 +2547,38 @@ func testAccResourceVSphereVirtualMachinePreCheck(t *testing.T) { t.Skip("set TF_VAR_VSPHERE_CONTENT_LIBRARY_FILES to run vsphere_virtual_machine acceptance tests") } } - +func testAccResourceVSphereVirtualMachinePreInstantCloneCheck(t *testing.T) { + // Note that TF_VAR_VSPHERE_USE_INSTANT_CLONE is also a variable and its presence + // speeds up tests greatly, but it's not a necessary variable, so we don't + // enforce it here. + if os.Getenv("TF_VAR_VSPHERE_DATACENTER") == "" { + t.Skip("set TF_VAR_VSPHERE_DATACENTER to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_CLUSTER") == "" { + t.Skip("set TF_VAR_VSPHERE_CLUSTER to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_PG_NAME") == "" { + t.Skip("set TF_VAR_VSPHERE_NETWORK_LABEL to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_NFS_DS_NAME") == "" { + t.Skip("set TF_VAR_VSPHERE_NFS_DS_NAME to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_RESOURCE_POOL") == "" { + t.Skip("set TF_VAR_VSPHERE_RESOURCE_POOL to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_TEMPLATE") == "" { + t.Skip("set TF_VAR_VSPHERE_TEMPLATE to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_ESXI1") == "" { + t.Skip("set TF_VAR_VSPHERE_ESXI_HOST to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_ESXI2") == "" { + t.Skip("set TF_VAR_VSPHERE_ESXI_HOST2 to run vsphere_virtual_machine acceptance tests") + } + if os.Getenv("TF_VAR_VSPHERE_USE_INSTANT_CLONE") == "" { + t.Skip("set TF_VAR_VSPHERE_USE_INSTANT_CLONE to run vsphere_virtual_machine acceptance tests") + } +} func testAccResourceVSphereVirtualMachineCheckExists(expected bool) resource.TestCheckFunc { return func(s *terraform.State) error { _, err := testGetVirtualMachine(s, "vm") @@ -3355,6 +3404,16 @@ func testAccResourceVSphereVirtualMachineConfigBase() string { testhelper.ConfigDataRootPortGroup1()) } +func testAccResourceVSphereVirtualMachineInstantCloneConfigBase() string { + return testhelper.CombineConfigs( + testhelper.ConfigDataRootDC1(), + testhelper.ConfigDataRootHost1(), + testhelper.ConfigDataRootHost2(), + testhelper.ConfigDataRootDS1(), + testhelper.ConfigDataRootComputeCluster1(), + testhelper.ConfigDataRootVMNet()) +} + func testAccResourceVSphereVirtualMachineConfigComputedValue() string { return fmt.Sprintf(`