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/vendor/github.com/vmware/govmomi/object/virtual_machine.go b/vendor/github.com/vmware/govmomi/object/virtual_machine.go index 2bcfab60b..f416c63ef 100644 --- a/vendor/github.com/vmware/govmomi/object/virtual_machine.go +++ b/vendor/github.com/vmware/govmomi/object/virtual_machine.go @@ -181,6 +181,21 @@ func (v VirtualMachine) Clone(ctx context.Context, folder *Folder, name string, return NewTask(v.c, res.Returnval), nil } +// InstantClone operation being called +func (v VirtualMachine) InstantClone(ctx context.Context, config types.VirtualMachineInstantCloneSpec) (*Task, error) { + req := types.InstantClone_Task{ + This: v.Reference(), + Spec: config, + } + + res, err := methods.InstantClone_Task(ctx, v.c, &req) + if err != nil { + return nil, err + } + + return NewTask(v.c, res.Returnval), nil +} + func (v VirtualMachine) Customize(ctx context.Context, spec types.CustomizationSpec) (*Task, error) { req := types.CustomizeVM_Task{ This: v.Reference(), 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 dbd55b253..498b183c3 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 97804b394..649078669 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/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/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, @@ -258,3 +265,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 0692a79ea..c2fed0153 100644 --- a/vsphere/resource_vsphere_virtual_machine.go +++ b/vsphere/resource_vsphere_virtual_machine.go @@ -518,19 +518,21 @@ 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 - } - // Network devices - if err := virtualdevice.NetworkInterfaceRefreshOperation(d, client, devices); err != nil { - return err - } - // CDROM - if err := virtualdevice.CdromRefreshOperation(d, client, devices); err != nil { - return err - } + if !d.Get("clone.0.instant_clone").(bool) { + // Disks first + if err := virtualdevice.DiskRefreshOperation(d, client, devices); err != nil { + return err + } + // Network devices + if err := virtualdevice.NetworkInterfaceRefreshOperation(d, client, devices); err != nil { + return err + } + // CDROM + if err := virtualdevice.CdromRefreshOperation(d, client, devices); err != nil { + return err + } + } // Read tags if we have the ability to do so if tagsClient, _ := meta.(*VSphereClient).TagsManager(); tagsClient != nil { if err := readTagsForResource(tagsClient, vm, d); err != nil { @@ -1437,6 +1439,20 @@ 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, resourceVSphereVirtualMachinePostDeployInstantCloneChanges(d, meta, vm) + } switch contentlibrary.IsContentLibraryItem(meta.(*VSphereClient).restClient, d.Get("clone.0.template_uuid").(string)) { case true: deploySpec, err := createVCenterDeploy(d, meta) @@ -1624,6 +1640,34 @@ func resourceVSphereVirtualMachinePostDeployChanges(d *schema.ResourceData, meta return nil } +// resourceVSphereVirtualMachinePostDeployInstantCloneChanges will do post-clone +// configuration for instant clone, and while the resource should have an ID until this is +// done, we need it to go through post-clone rollback workflows. All +// rollback functions will remove the ID after it has done its rollback. +// +// It's generally safe to not rollback after the initial re-configuration is +// fully complete and we move on to sending the customization spec. +func resourceVSphereVirtualMachinePostDeployInstantCloneChanges(d *schema.ResourceData, meta interface{}, vm *object.VirtualMachine) error { + vprops, err := virtualmachine.Properties(vm) + if err != nil { + return resourceVSphereVirtualMachineRollbackCreate( + d, + meta, + vm, + fmt.Errorf("cannot fetch properties of created virtual machine: %s", err), + ) + } + log.Printf("[DEBUG] VM %q - UUID is %q", vm.InventoryPath, vprops.Config.Uuid) + d.SetId(vprops.Config.Uuid) + vmprops, err := virtualmachine.Properties(vm) + if err != nil { + return err + } + // This should only change if deploying from a Content Library item. + d.Set("guest_id", vmprops.Config.GuestId) + return nil +} + // resourceVSphereVirtualMachineCreateCloneWithSDRS runs the clone part of // resourceVSphereVirtualMachineCreateClone through storage DRS. It's designed // to be run when a storage cluster is specified, versus simply specifying diff --git a/vsphere/resource_vsphere_virtual_machine_test.go b/vsphere/resource_vsphere_virtual_machine_test.go index 4976e07b4..3d4316b86 100644 --- a/vsphere/resource_vsphere_virtual_machine_test.go +++ b/vsphere/resource_vsphere_virtual_machine_test.go @@ -16,6 +16,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/testhelper" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/testhelper" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/computeresource" @@ -1748,6 +1751,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() { @@ -2631,7 +2652,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") @@ -3665,6 +3717,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(` @@ -5560,6 +5622,48 @@ resource "vsphere_virtual_machine" "vm" { ) } +func testAccResourceVSphereVirtualMachineConfigInstant() 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 = "${data.vsphere_compute_cluster.rootcompute_cluster1.resource_pool_id}" + datastore_id = "${data.vsphere_datastore.rootds1.id}" + wait_for_guest_net_timeout = -1 + guest_id = "${data.vsphere_virtual_machine.template.guest_id}" + + network_interface { + network_id = "${data.vsphere_network.vmnet.id}" + } + + disk { + label = "disk0" + size = 20 + } + + clone { + template_uuid = "${data.vsphere_virtual_machine.template.id}" + instant_clone = true + } + +} + + `, + testAccResourceVSphereVirtualMachineInstantCloneConfigBase(), + os.Getenv("TF_VAR_VSPHERE_TEMPLATE"), + os.Getenv("TF_VAR_VSPHERE_USE_INSTANT_CLONE"), + ) +} + func testAccResourceVSphereVirtualMachineConfigBadSizeUnlinked() string { return fmt.Sprintf(`