From ad02e3bba2b56bd80c336646a91afaf87fd799fc Mon Sep 17 00:00:00 2001 From: Anant Chopra Date: Tue, 16 Nov 2021 01:44:21 +0530 Subject: [PATCH] feat: instant clone Adds support for instant clones. --- .../helper/storagepod/storage_pod_helper.go | 54 +++++++++ .../virtualmachine/virtual_machine_helper.go | 24 ++++ .../virtual_machine_clone_subresource.go | 74 ++++++++++++ vsphere/resource_vsphere_virtual_machine.go | 26 +++- .../resource_vsphere_virtual_machine_test.go | 113 ++++++++++++++++++ website/docs/r/virtual_machine.html.markdown | 8 +- 6 files changed, 294 insertions(+), 5 deletions(-) diff --git a/vsphere/internal/helper/storagepod/storage_pod_helper.go b/vsphere/internal/helper/storagepod/storage_pod_helper.go index f8ca01d84..1bbbf96b2 100644 --- a/vsphere/internal/helper/storagepod/storage_pod_helper.go +++ b/vsphere/internal/helper/storagepod/storage_pod_helper.go @@ -18,6 +18,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" ) @@ -521,3 +522,56 @@ func IsMember(pod *object.StoragePod, ds *object.Datastore) (bool, error) { } return true, nil } + +// get_recommended_datastore from storagePod +func GetRecommendDatastore(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 55c0a7242..ce246616f 100644 --- a/vsphere/internal/helper/virtualmachine/virtual_machine_helper.go +++ b/vsphere/internal/helper/virtualmachine/virtual_machine_helper.go @@ -546,6 +546,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 079beff2b..c0606aa0b 100644 --- a/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go +++ b/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go @@ -11,8 +11,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" @@ -41,6 +43,11 @@ func VirtualMachineCloneSchema() map[string]*schema.Schema { Required: true, Description: "The UUID of the source virtual machine or template.", }, + "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.", + }, "linked_clone": { Type: schema.TypeBool, Optional: true, @@ -309,3 +316,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.GetRecommendDatastore(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 0d600609d..bcbe06249 100644 --- a/vsphere/resource_vsphere_virtual_machine.go +++ b/vsphere/resource_vsphere_virtual_machine.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" "log" "net" "os" @@ -15,6 +14,8 @@ import ( "strings" "time" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/contentlibrary" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/ovfdeploy" @@ -1494,6 +1495,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) @@ -1760,9 +1777,12 @@ func resourceVSphereVirtualMachinePostDeployChanges(d *schema.ResourceData, meta } // 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 { log.Printf("[DEBUG] %s: Waiting for VM customization to complete", resourceVSphereVirtualMachineIDString(d)) diff --git a/vsphere/resource_vsphere_virtual_machine_test.go b/vsphere/resource_vsphere_virtual_machine_test.go index 23a439637..4be77e900 100644 --- a/vsphere/resource_vsphere_virtual_machine_test.go +++ b/vsphere/resource_vsphere_virtual_machine_test.go @@ -1788,6 +1788,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: testAccResourceVSphereVirtualMachineConfigInstantClone(), + Check: testAccResourceVSphereVirtualMachineCheckExists(true), + }, + }, + }) +} + func TestAccResourceVSphereVirtualMachine_cloneWithBadSizeWithoutLinkedClone(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { @@ -2688,6 +2706,39 @@ func testAccSriovPreCheck(t *testing.T) { } } +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") @@ -3596,6 +3647,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(` @@ -7218,6 +7279,58 @@ resource "vsphere_virtual_machine" "vm" { ) } +func testAccResourceVSphereVirtualMachineConfigInstantClone() string { + return fmt.Sprintf(` +%s // Mix and match config + +data "vsphere_virtual_machine" "template" { + name = "%s" + datacenter_id = data.vsphere_datacenter.rootdc1.id +} + +variable "instant_clone" { + default = "%s" +} + +resource "vsphere_virtual_machine" "vm" { + name = "testacc-test" + resource_pool_id = "${vsphere_resource_pool.pool1.id}" + datastore_id = vsphere_nas_datastore.ds1.id + + num_cpus = 2 + memory = 2048 + guest_id = "${data.vsphere_virtual_machine.template.guest_id}" + + wait_for_guest_net_timeout = -1 + + network_interface { + network_id = "${data.vsphere_network.network1.id}" + adapter_type = "${data.vsphere_virtual_machine.template.network_interface_types[0]}" + } + + disk { + label = "disk0" + size = "${data.vsphere_virtual_machine.template.disks.0.size}" + eagerly_scrub = "${data.vsphere_virtual_machine.template.disks.0.eagerly_scrub}" + thin_provisioned = "${data.vsphere_virtual_machine.template.disks.0.thin_provisioned}" + } + + clone { + template_uuid = "${data.vsphere_virtual_machine.template.id}" + instant_clone = "${var.instant_clone != "" ? "true" : "false" }" + } + + cdrom { + client_device = true + } +} +`, + testAccResourceVSphereVirtualMachineConfigBase(), + os.Getenv("TF_VAR_VSPHERE_TEMPLATE"), + os.Getenv("TF_VAR_VSPHERE_USE_INSTANT_CLONE"), + ) +} + func testAccResourceVSphereVirtualMachineConfigHighSensitivity() string { return fmt.Sprintf(` diff --git a/website/docs/r/virtual_machine.html.markdown b/website/docs/r/virtual_machine.html.markdown index 00fec612e..efc5ee10a 100644 --- a/website/docs/r/virtual_machine.html.markdown +++ b/website/docs/r/virtual_machine.html.markdown @@ -32,6 +32,8 @@ Virtual disks can be SCSI, SATA, or IDE. The storage controllers managed by the When cloning from a template, you must specify disks of either the same or greater size than the disks in the source template or the same size when cloning from a snapshot (also known as a linked clone). +When cloning from a powered-on virtual machine ,you must specify disks of either the same or greater size than the disks in the parent virtual machine(also known as a instant clone). + See the section on [Creating a Virtual Machine from a Template](#creating-a-virtual-machine-from-a-template) for more information. ### Customization and Network Waiters @@ -1061,7 +1063,7 @@ The options are: ## Creating a Virtual Machine from a Template -The `clone` block can be used to create a new virtual machine from an existing virtual machine or template. The resource supports both making a complete copy of a virtual machine, or cloning from a snapshot (also known as a linked clone). +The `clone` block can be used to create a new virtual machine from an existing virtual machine or template. The resource supports both making a complete copy of a virtual machine, or cloning from a snapshot (also known as a linked clone).The resource is also used to create powered-on virtual machines from the running state of another powered-on virtual machine (also known as a instant clone). See the section on [cloning and customization](#cloning-and-customization) for more information. @@ -1073,7 +1075,9 @@ The options available in the `clone` block are: * `template_uuid` - (Required) The UUID of the source virtual machine or template. -* `linked_clone` - (Optional) Clone the virtual machine from a snapshot or a template. Default: `false`. +* `instant_clone` - (Optional) Clone the virtual machine from a running powered on virtual machine. Supported on vSphere 6.7 and later. Default: `false`. Only one of `instant_clone` or `linked_clone` can be set to `true`. + +* `linked_clone` - (Optional) Clone the virtual machine from a snapshot or a template. Default: `false`. Only one of `linked_clone` or `instant_clone` can be set to `true`. * `timeout` - (Optional) The timeout, in minutes, to wait for the cloning process to complete. Default: 30 minutes.