Skip to content

Commit

Permalink
feature: instant_clone
Browse files Browse the repository at this point in the history
  • Loading branch information
Anant Chopra committed Jun 3, 2021
1 parent da0c688 commit 7196cee
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 3 deletions.
1 change: 1 addition & 0 deletions tf-vsphere-devrc.mk.example
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]'
Expand Down
54 changes: 54 additions & 0 deletions vsphere/internal/helper/storagepod/storage_pod_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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

}
24 changes: 24 additions & 0 deletions vsphere/internal/helper/virtualmachine/virtual_machine_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
74 changes: 74 additions & 0 deletions vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
24 changes: 22 additions & 2 deletions vsphere/resource_vsphere_virtual_machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
61 changes: 60 additions & 1 deletion vsphere/resource_vsphere_virtual_machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(`
Expand Down

0 comments on commit 7196cee

Please sign in to comment.