Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: instant_clone #1371

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1443,6 +1444,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 @@ -1623,10 +1640,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 @@ -1782,6 +1782,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 @@ -2551,7 +2569,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 @@ -3407,6 +3456,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