Repository: vmware/govmomi
Pull request #3590 (2024-10-10 15:20:14)

govc: Add an unreleased folder.place command for PlaceVMsXCluster
This change adds an unreleased govc command to exercise the cross-cluster placement engine via folder.PlaceVmsXCluster.  This command is limited to supporting the only a relocate placement type but will be extended to support createAndPowerOn and reconfigure in the future.  Bats tests are added to perform basic validation of the new command.

This change also fixes the PlaceVMSXCluster action types to match the types returned by the VIM API, specifically:
ClusterReconfigurePlacementAction => ClusterClusterReconfigurePlacementAction ClusterRelocatePlacementAction => ClusterClusterRelocatePlacementAction

This change also improves the simulator to return the appropriate action types based off of the placementType and improves the tests to validate the action type.

Testing Done:
./govc/test/folder.bats
go test -v -count=1 ./simulator -run TestPlaceVmsXClusterCreateAndPowerOn
go test -v -count=1 ./simulator -run TestPlaceVmsXClusterReconfigure
go test -v -count=1 ./simulator -run TestPlaceVmsXClusterRelocate



Patch:
diff --git a/govc/folder/place.go b/govc/folder/place.go
new file mode 100644
index 00000000..2c02b8a4
--- /dev/null
+++ b/govc/folder/place.go
@@ -0,0 +1,407 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package folder
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+var allTypes = []string{}
+
+var createAndPowerOnTypes = []string{
+	string(types.PlaceVmsXClusterSpecPlacementTypeCreateAndPowerOn),
+}
+
+var relocateTypes = []string{
+	string(types.PlaceVmsXClusterSpecPlacementTypeRelocate),
+}
+
+var reconfigureTypes = []string{
+	string(types.PlaceVmsXClusterSpecPlacementTypeReconfigure),
+}
+
+func init() {
+	allTypes = append(allTypes, createAndPowerOnTypes...)
+	allTypes = append(allTypes, relocateTypes...)
+	allTypes = append(allTypes, reconfigureTypes...)
+}
+
+type typeFlag string
+
+func (t *typeFlag) Set(s string) error {
+	s = strings.ToLower(s)
+	for _, e := range allTypes {
+		if s == strings.ToLower(e) {
+			*t = typeFlag(e)
+			return nil
+		}
+	}
+
+	return fmt.Errorf("unknown type")
+}
+
+func (t *typeFlag) String() string {
+	return string(*t)
+}
+
+func (t *typeFlag) partOf(m []string) bool {
+	for _, e := range m {
+		if t.String() == e {
+			return true
+		}
+	}
+	return false
+}
+
+func (t *typeFlag) IsCreateAndPowerOnType() bool {
+	return t.partOf(createAndPowerOnTypes)
+}
+
+func (t *typeFlag) IsRelocateType() bool {
+	return t.partOf(relocateTypes)
+}
+
+func (t *typeFlag) IsReconfigureType() bool {
+	return t.partOf(reconfigureTypes)
+}
+
+type place struct {
+	*flags.ClientFlag
+	*flags.DatacenterFlag
+	*flags.VirtualMachineFlag
+	*flags.OutputFlag
+
+	pool flags.StringList
+	Type typeFlag
+}
+
+func init() {
+	cli.Register("folder.place", &place{}, true)
+}
+
+func (cmd *place) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
+	cmd.DatacenterFlag.Register(ctx, f)
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+
+	f.Var(&cmd.pool, "pool", "Resource Pools to use for placement.")
+	f.Var(&cmd.Type, "type", fmt.Sprintf("Placement type (%s)", strings.Join(allTypes, "|")))
+}
+
+func (cmd *place) Usage() string {
+	return "PATH..."
+}
+
+func (cmd *place) Description() string {
+	return `Get a placement recommendation for an existing VM
+
+Examples:
+  govc folder.place -rp $rp1Name -rp $rp2Name -rp $rp3Name-vm $vmName`
+}
+
+func (cmd *place) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.DatacenterFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (cmd *place) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	// Use latest version to pick up latest PlaceVmsXCluster API.
+	err = c.UseServiceVersion()
+	if err != nil {
+		return err
+	}
+
+	vm, err := cmd.VirtualMachine()
+	if err != nil {
+		return err
+	}
+
+	if vm == nil {
+		return flag.ErrHelp
+	}
+
+	finder, err := cmd.Finder()
+	if err != nil {
+		return err
+	}
+
+	var relocateSpec *types.VirtualMachineRelocateSpec
+
+	// TODO: Support createAndPowerOn and reconfigure.
+	switch {
+	case cmd.Type.IsRelocateType():
+		relocateSpec = &types.VirtualMachineRelocateSpec{}
+		break
+	case cmd.Type.IsReconfigureType():
+		return errors.New("reconfigure is currently an unsupported placement type")
+	case cmd.Type.IsCreateAndPowerOnType():
+		return errors.New("createAndPowerOn is currently an unsupported placement type")
+	default:
+		return errors.New("please specify a valid type")
+	}
+
+	// PlaceVMsXCluster is only valid against the root folder.
+	folder := object.NewRootFolder(c)
+
+	refs := make([]types.ManagedObjectReference, 0, len(cmd.pool))
+
+	for _, arg := range cmd.pool {
+		rp, err := finder.ResourcePool(ctx, arg)
+		if err != nil {
+			return err
+		}
+
+		refs = append(refs, rp.Reference())
+	}
+
+	vmPlacementSpecs := []types.PlaceVmsXClusterSpecVmPlacementSpec{{
+		Vm:           types.NewReference(vm.Reference()),
+		ConfigSpec:   types.VirtualMachineConfigSpec{},
+		RelocateSpec: relocateSpec,
+	}}
+
+	placementSpec := types.PlaceVmsXClusterSpec{
+		ResourcePools:           refs,
+		PlacementType:           cmd.Type.String(),
+		VmPlacementSpecs:        vmPlacementSpecs,
+		HostRecommRequired:      types.NewBool(true),
+		DatastoreRecommRequired: types.NewBool(true),
+	}
+
+	res, err := folder.PlaceVmsXCluster(ctx, placementSpec)
+	if err != nil {
+		return err
+	}
+
+	vimClient, err := cmd.ClientFlag.Client()
+	if err != nil {
+		return err
+	}
+
+	return cmd.WriteResult(&placementResult{res, vimClient, ctx, cmd.VirtualMachineFlag})
+}
+
+type placementResult struct {
+	Result    *types.PlaceVmsXClusterResult `json:"result,omitempty"`
+	vimClient *vim25.Client
+	ctx       context.Context
+	vm        *flags.VirtualMachineFlag
+}
+
+func (res *placementResult) Dump() interface{} {
+	return res.Result
+}
+
+func (res *placementResult) initialPlacementAction(w io.Writer, pinfo types.PlaceVmsXClusterResultPlacementInfo, action *types.ClusterClusterInitialPlacementAction) error {
+
+	spec := action.ConfigSpec
+	if spec == nil {
+		return nil
+	}
+
+	fields := []struct {
+		name string
+		moid *types.ManagedObjectReference
+	}{
+		{"Vm", pinfo.Vm},
+		{"  Target", pinfo.Recommendation.Target},
+		{"  TargetHost", action.TargetHost},
+		{"  Pool", &action.Pool},
+	}
+
+	for _, f := range fields {
+		if f.moid == nil {
+			continue
+		}
+		path, err := find.InventoryPath(res.ctx, res.vimClient, *f.moid)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(w, "%s:\t%s\n", f.name, path)
+	}
+
+	return nil
+}
+
+func (res *placementResult) relocatePlacementAction(w io.Writer, pinfo types.PlaceVmsXClusterResultPlacementInfo, action *types.ClusterClusterRelocatePlacementAction) error {
+
+	spec := action.RelocateSpec
+	if spec == nil {
+		return nil
+	}
+
+	fields := []struct {
+		name string
+		moid *types.ManagedObjectReference
+	}{
+		{"Vm", pinfo.Vm},
+		{"  Target", pinfo.Recommendation.Target},
+		{"  Folder", spec.Folder},
+		{"  Datastore", spec.Datastore},
+		{"  Pool", spec.Pool},
+		{"  Host", spec.Host},
+	}
+
+	for _, f := range fields {
+		if f.moid == nil {
+			continue
+		}
+		path, err := find.InventoryPath(res.ctx, res.vimClient, *f.moid)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(w, "%s:\t%s\n", f.name, path)
+	}
+
+	return nil
+}
+
+func (res *placementResult) reconfigurePlacementAction(w io.Writer, pinfo types.PlaceVmsXClusterResultPlacementInfo, action *types.ClusterClusterReconfigurePlacementAction) error {
+
+	spec := action.ConfigSpec
+	if spec == nil {
+		return nil
+	}
+
+	fields := []struct {
+		name string
+		moid *types.ManagedObjectReference
+	}{
+		{"Vm", pinfo.Vm},
+		{"  Target", pinfo.Recommendation.Target},
+		{"  TargetHost", action.TargetHost},
+		{"  Pool", &action.Pool},
+	}
+
+	for _, f := range fields {
+		if f.moid == nil {
+			continue
+		}
+		path, err := find.InventoryPath(res.ctx, res.vimClient, *f.moid)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(w, "%s:\t%s\n", f.name, path)
+	}
+
+	return nil
+}
+
+func (res *placementResult) placementFault(w io.Writer, pfault types.PlaceVmsXClusterResultPlacementFaults, fault *types.LocalizedMethodFault) error {
+
+	fields := []struct {
+		name    string
+		message string
+		moid    *types.ManagedObjectReference
+	}{
+		{"Vm", "", pfault.Vm},
+		{"  Message", fault.LocalizedMessage, nil},
+	}
+
+	for _, f := range fields {
+		if f.moid == nil {
+			if f.message != "" {
+				fmt.Fprintf(w, "%s:\t%s\n", f.name, f.message)
+			}
+			continue
+		}
+		path, err := find.InventoryPath(res.ctx, res.vimClient, *f.moid)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(w, "%s:\t%s\n", f.name, path)
+	}
+
+	return nil
+}
+
+func (res placementResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	for _, pinfo := range res.Result.PlacementInfos {
+
+		for _, action := range pinfo.Recommendation.Action {
+
+			if initPlaceAction, ok := action.(*types.ClusterClusterInitialPlacementAction); ok {
+				err := res.initialPlacementAction(w, pinfo, initPlaceAction)
+				if err != nil {
+					return err
+				}
+			}
+
+			if relocateAction, ok := action.(*types.ClusterClusterRelocatePlacementAction); ok {
+				err := res.relocatePlacementAction(w, pinfo, relocateAction)
+				if err != nil {
+					return err
+				}
+			}
+
+			if reconfigureAction, ok := action.(*types.ClusterClusterReconfigurePlacementAction); ok {
+				err := res.reconfigurePlacementAction(w, pinfo, reconfigureAction)
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	for _, pfault := range res.Result.Faults {
+
+		for _, fault := range pfault.Faults {
+			err := res.placementFault(w, pfault, &fault)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return tw.Flush()
+}
diff --git a/object/virtual_device_list.go b/object/virtual_device_list.go
index 4d4335ea..295171af 100644
--- a/object/virtual_device_list.go
+++ b/object/virtual_device_list.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2015-2024 VMware, Inc. All Rights Reserved.
+Copyright (c) 2015-2017 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -545,8 +545,6 @@ func (l VirtualDeviceList) AssignController(device types.BaseVirtualDevice, c ty
 	if d.Key == 0 {
 		d.Key = l.newRandomKey()
 	}
-
-	c.GetVirtualController().Device = append(c.GetVirtualController().Device, d.Key)
 }
 
 // newRandomKey returns a random negative device key.
diff --git a/simulator/folder.go b/simulator/folder.go
index d4e2c281..4d6e96ec 100644
--- a/simulator/folder.go
+++ b/simulator/folder.go
@@ -786,11 +786,11 @@ func addPlacementFault(body *methods.PlaceVmsXClusterBody, vmName string, vmRef
 	body.Res.Returnval.Faults = append(body.Res.Returnval.Faults, faults)
 }
 
-func generateInitialPlacementAction(ctx *Context, inputConfigSpec *types.VirtualMachineConfigSpec, pool *ResourcePool,
-	cluster *ClusterComputeResource, hostRequired, datastoreRequired bool) *types.ClusterClusterInitialPlacementAction {
-	var configSpec *types.VirtualMachineConfigSpec
+func generateRelocatePlacementAction(ctx *Context, inputRelocateSpec *types.VirtualMachineRelocateSpec, pool *ResourcePool,
+	cluster *ClusterComputeResource, hostRequired, datastoreRequired bool) *types.ClusterClusterRelocatePlacementAction {
+	var relocateSpec *types.VirtualMachineRelocateSpec
 
-	placementAction := types.ClusterClusterInitialPlacementAction{
+	placementAction := types.ClusterClusterRelocatePlacementAction{
 		Pool: pool.Self,
 	}
 
@@ -800,49 +800,18 @@ func generateInitialPlacementAction(ctx *Context, inputConfigSpec *types.Virtual
 	}
 
 	if datastoreRequired {
-		configSpec = inputConfigSpec
+		relocateSpec = inputRelocateSpec
 
-		// TODO: This is just an initial implementation aimed at returning some data but it is not
-		// necessarily fully consistent, like we should ensure the host, if also required, has the
-		// datastore mounted.
 		ds := ctx.Map.Get(cluster.Datastore[rand.Intn(len(cluster.Datastore))]).(*Datastore)
 
-		if configSpec.Files == nil {
-			configSpec.Files = new(types.VirtualMachineFileInfo)
-		}
-		configSpec.Files.VmPathName = fmt.Sprintf("[%[1]s] %[2]s/%[2]s.vmx", ds.Name, inputConfigSpec.Name)
-
-		for _, change := range configSpec.DeviceChange {
-			dspec := change.GetVirtualDeviceConfigSpec()
+		relocateSpec.Datastore = types.NewReference(ds.Reference())
 
-			if dspec.FileOperation != types.VirtualDeviceConfigSpecFileOperationCreate {
-				continue
-			}
-
-			switch dspec.Operation {
-			case types.VirtualDeviceConfigSpecOperationAdd:
-				device := dspec.Device
-				d := device.GetVirtualDevice()
-
-				switch device.(type) {
-				case *types.VirtualDisk:
-					switch b := d.Backing.(type) {
-					case types.BaseVirtualDeviceFileBackingInfo:
-						info := b.GetVirtualDeviceFileBackingInfo()
-						info.Datastore = types.NewReference(ds.Reference())
-
-						var dsPath object.DatastorePath
-						if dsPath.FromString(info.FileName) {
-							dsPath.Datastore = ds.Name
-							info.FileName = dsPath.String()
-						}
-					}
-				}
-			}
+		for _, diskLocator := range relocateSpec.Disk {
+			diskLocator.Datastore = ds.Reference()
 		}
 	}
 
-	placementAction.ConfigSpec = configSpec
+	placementAction.RelocateSpec = relocateSpec
 	return &placementAction
 }
 
@@ -895,7 +864,7 @@ func generateRecommendationForRelocate(ctx *Context, req *types.PlaceVmsXCluster
 			Target:     &cluster.Self,
 		}
 
-		placementAction := generateInitialPlacementAction(ctx, &spec.ConfigSpec, pool, cluster, hostRequired, datastoreRequired)
+		placementAction := generateRelocatePlacementAction(ctx, spec.RelocateSpec, pool, cluster, hostRequired, datastoreRequired)
 
 		reco.Action = append(reco.Action, placementAction)
 
@@ -910,6 +879,31 @@ func generateRecommendationForRelocate(ctx *Context, req *types.PlaceVmsXCluster
 	return body
 }
 
+func generateReconfigurePlacementAction(ctx *Context, inputConfigSpec *types.VirtualMachineConfigSpec, pool *ResourcePool,
+	cluster *ClusterComputeResource, hostRequired, datastoreRequired bool) *types.ClusterClusterReconfigurePlacementAction {
+	var configSpec *types.VirtualMachineConfigSpec
+
+	placementAction := types.ClusterClusterReconfigurePlacementAction{
+		Pool: pool.Self,
+	}
+
+	if hostRequired {
+		randomHost := cluster.Host[rand.Intn(len(cluster.Host))]
+		placementAction.TargetHost = &randomHost
+	}
+
+	if datastoreRequired {
+		configSpec = inputConfigSpec
+
+		ds := ctx.Map.Get(cluster.Datastore[rand.Intn(len(cluster.Datastore))]).(*Datastore)
+
+		fillConfigSpecWithDatastore(ctx, inputConfigSpec, configSpec, ds)
+	}
+
+	placementAction.ConfigSpec = configSpec
+	return &placementAction
+}
+
 func generateRecommendationForReconfigure(ctx *Context, req *types.PlaceVmsXCluster) *methods.PlaceVmsXClusterBody {
 
 	pools := req.PlacementSpec.ResourcePools
@@ -969,7 +963,7 @@ func generateRecommendationForReconfigure(ctx *Context, req *types.PlaceVmsXClus
 			Target:     &cluster.Self,
 		}
 
-		placementAction := generateInitialPlacementAction(ctx, &spec.ConfigSpec, pool, cluster, hostRequired, datastoreRequired)
+		placementAction := generateReconfigurePlacementAction(ctx, &spec.ConfigSpec, pool, cluster, hostRequired, datastoreRequired)
 
 		reco.Action = append(reco.Action, placementAction)
 
@@ -984,6 +978,70 @@ func generateRecommendationForReconfigure(ctx *Context, req *types.PlaceVmsXClus
 	return body
 }
 
+func fillConfigSpecWithDatastore(ctx *Context, inputConfigSpec, configSpec *types.VirtualMachineConfigSpec, ds *Datastore) {
+	if configSpec.Files == nil {
+		configSpec.Files = new(types.VirtualMachineFileInfo)
+	}
+	configSpec.Files.VmPathName = fmt.Sprintf("[%[1]s] %[2]s/%[2]s.vmx", ds.Name, inputConfigSpec.Name)
+
+	for _, change := range configSpec.DeviceChange {
+		dspec := change.GetVirtualDeviceConfigSpec()
+
+		if dspec.FileOperation != types.VirtualDeviceConfigSpecFileOperationCreate {
+			continue
+		}
+
+		switch dspec.Operation {
+		case types.VirtualDeviceConfigSpecOperationAdd:
+			device := dspec.Device
+			d := device.GetVirtualDevice()
+
+			switch device.(type) {
+			case *types.VirtualDisk:
+				switch b := d.Backing.(type) {
+				case types.BaseVirtualDeviceFileBackingInfo:
+					info := b.GetVirtualDeviceFileBackingInfo()
+					info.Datastore = types.NewReference(ds.Reference())
+
+					var dsPath object.DatastorePath
+					if dsPath.FromString(info.FileName) {
+						dsPath.Datastore = ds.Name
+						info.FileName = dsPath.String()
+					}
+				}
+			}
+		}
+	}
+}
+
+func generateInitialPlacementAction(ctx *Context, inputConfigSpec *types.VirtualMachineConfigSpec, pool *ResourcePool,
+	cluster *ClusterComputeResource, hostRequired, datastoreRequired bool) *types.ClusterClusterInitialPlacementAction {
+	var configSpec *types.VirtualMachineConfigSpec
+
+	placementAction := types.ClusterClusterInitialPlacementAction{
+		Pool: pool.Self,
+	}
+
+	if hostRequired {
+		randomHost := cluster.Host[rand.Intn(len(cluster.Host))]
+		placementAction.TargetHost = &randomHost
+	}
+
+	if datastoreRequired {
+		configSpec = inputConfigSpec
+
+		// TODO: This is just an initial implementation aimed at returning some data but it is not
+		// necessarily fully consistent, like we should ensure the host, if also required, has the
+		// datastore mounted.
+		ds := ctx.Map.Get(cluster.Datastore[rand.Intn(len(cluster.Datastore))]).(*Datastore)
+
+		fillConfigSpecWithDatastore(ctx, inputConfigSpec, configSpec, ds)
+	}
+
+	placementAction.ConfigSpec = configSpec
+	return &placementAction
+}
+
 func generateRecommendationForCreateAndPowerOn(ctx *Context, req *types.PlaceVmsXCluster) *methods.PlaceVmsXClusterBody {
 
 	pools := req.PlacementSpec.ResourcePools
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 68f4528c..a596798d 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -1279,28 +1279,12 @@ func (vm *VirtualMachine) configureDevice(
 	d := device.GetVirtualDevice()
 	var controller types.BaseVirtualController
 
-	key := d.Key
 	if d.Key <= 0 {
 		// Keys can't be negative; Key 0 is reserved
 		d.Key = devices.NewKey()
 		d.Key *= -1
 	}
 
-	// Update device controller's key reference
-	if key != d.Key {
-		if device := devices.FindByKey(d.ControllerKey); device != nil {
-			if c, ok := device.(types.BaseVirtualController); ok {
-				c := c.GetVirtualController()
-				for i := range c.Device {
-					if c.Device[i] == key {
-						c.Device[i] = d.Key
-						break
-					}
-				}
-			}
-		}
-	}
-
 	// Choose a unique key
 	for {
 		if devices.FindByKey(d.Key) == nil {
diff --git a/vim25/types/unreleased.go b/vim25/types/unreleased.go
index 57bc82a1..094ac395 100644
--- a/vim25/types/unreleased.go
+++ b/vim25/types/unreleased.go
@@ -153,7 +153,7 @@ func init() {
 
 const RecommendationReasonCodeXClusterPlacement = RecommendationReasonCode("xClusterPlacement")
 
-type ClusterReconfigurePlacementAction struct {
+type ClusterClusterReconfigurePlacementAction struct {
 	ClusterAction
 	TargetHost *ManagedObjectReference   `xml:"targetHost,omitempty"`
 	Pool       ManagedObjectReference    `xml:"pool"`
@@ -161,10 +161,10 @@ type ClusterReconfigurePlacementAction struct {
 }
 
 func init() {
-	t["ClusterReconfigurePlacementAction"] = reflect.TypeOf((*ClusterReconfigurePlacementAction)(nil)).Elem()
+	t["ClusterClusterReconfigurePlacementAction"] = reflect.TypeOf((*ClusterClusterReconfigurePlacementAction)(nil)).Elem()
 }
 
-type ClusterRelocatePlacementAction struct {
+type ClusterClusterRelocatePlacementAction struct {
 	ClusterAction
 	TargetHost   *ManagedObjectReference     `xml:"targetHost,omitempty"`
 	Pool         ManagedObjectReference      `xml:"pool"`
@@ -172,5 +172,5 @@ type ClusterRelocatePlacementAction struct {
 }
 
 func init() {
-	t["ClusterRelocatePlacementAction"] = reflect.TypeOf((*ClusterRelocatePlacementAction)(nil)).Elem()
+	t["ClusterClusterRelocatePlacementAction"] = reflect.TypeOf((*ClusterClusterRelocatePlacementAction)(nil)).Elem()
 }


Test patch:
diff --git a/govc/test/folder.bats b/govc/test/folder.bats
index 22048764..216b0e29 100755
--- a/govc/test/folder.bats
+++ b/govc/test/folder.bats
@@ -77,3 +77,37 @@ load test_helper
     run govc object.destroy /$name
     assert_success
 }
+
+@test "folder.place" {
+    vcsim_env
+
+    export GOVC_SHOW_UNRELEASED=true
+
+    run govc folder.place
+    assert_failure # -vm must be specified
+
+    run govc folder.place -vm invalid
+    assert_failure # vm must be valid
+
+    vmid=$(new_id)
+    run govc vm.create -cluster DC0_C0 "$vmid"
+    assert_success
+
+    run govc folder.place $name -vm $id
+    assert_failure # -pool must be specified
+
+    poolid=$(new_id)
+
+    path="/DC0/host/DC0_C0/Resources/$poolid"
+    run govc pool.create $path
+    assert_success
+
+    run govc folder.place $name -vm $vmid -pool $poolid
+    assert_failure # -Type must be specified
+
+    run govc folder.place $name -vm $vmid -pool $poolid -type relocate
+    assert_success
+
+    run govc folder.place -json $name -vm $vmid -pool $poolid -type relocate
+    assert_success
+}
diff --git a/object/virtual_device_list_test.go b/object/virtual_device_list_test.go
index 9cfc3273..466fb02d 100644
--- a/object/virtual_device_list_test.go
+++ b/object/virtual_device_list_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2015-2024 VMware, Inc. All Rights Reserved.
+Copyright (c) 2015 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -744,12 +744,6 @@ func TestAssignController(t *testing.T) {
 	if disk.Key >= 0 {
 		t.Errorf("device key %d should be negative", disk.Key)
 	}
-
-	// AssignController should add the disk to the controller's device list.
-	cd := scsi.(*types.VirtualLsiLogicController).Device[0]
-	if cd != disk.Key {
-		t.Errorf("expected controller device key: %d, got: %d\n", disk.Key, cd)
-	}
 }
 
 func TestCreateSCSIController(t *testing.T) {
diff --git a/simulator/folder_test.go b/simulator/folder_test.go
index 796cbb37..0a8efa17 100644
--- a/simulator/folder_test.go
+++ b/simulator/folder_test.go
@@ -729,9 +729,13 @@ func TestPlaceVmsXClusterRelocate(t *testing.T) {
 		for testNo, test := range tests {
 			test := test // assign to local var since loop var is reused
 
+			truebool := true
+
 			placeVmsXClusterSpec := types.PlaceVmsXClusterSpec{
-				ResourcePools: test.poolMoRefs,
-				PlacementType: string(types.PlaceVmsXClusterSpecPlacementTypeRelocate),
+				ResourcePools:           test.poolMoRefs,
+				PlacementType:           string(types.PlaceVmsXClusterSpecPlacementTypeRelocate),
+				HostRecommRequired:      &truebool,
+				DatastoreRecommRequired: &truebool,
 			}
 
 			placeVmsXClusterSpec.VmPlacementSpecs = []types.PlaceVmsXClusterSpecVmPlacementSpec{{
@@ -752,7 +756,19 @@ func TestPlaceVmsXClusterRelocate(t *testing.T) {
 
 			if err == nil {
 				if len(res.PlacementInfos) != len(placeVmsXClusterSpec.VmPlacementSpecs) {
-					t.Errorf("%d PlacementInfos vs %d VmPlacementSpecs", len(res.PlacementInfos), len(placeVmsXClusterSpec.VmPlacementSpecs))
+					t.Errorf("Test %v: %d PlacementInfos vs %d VmPlacementSpecs", testNo, len(res.PlacementInfos), len(placeVmsXClusterSpec.VmPlacementSpecs))
+				}
+
+				for _, pinfo := range res.PlacementInfos {
+					for _, action := range pinfo.Recommendation.Action {
+						if relocateAction, ok := action.(*types.ClusterClusterRelocatePlacementAction); ok {
+							if relocateAction.TargetHost == nil {
+								t.Errorf("Test %v: received nil host recommendation", testNo)
+							}
+						} else {
+							t.Errorf("Test %v: received wrong action type in recommendation", testNo)
+						}
+					}
 				}
 			}
 		}
@@ -839,8 +855,10 @@ func TestPlaceVmsXClusterReconfigure(t *testing.T) {
 			test := test // assign to local var since loop var is reused
 
 			placeVmsXClusterSpec := types.PlaceVmsXClusterSpec{
-				ResourcePools: test.poolMoRefs,
-				PlacementType: string(types.PlaceVmsXClusterSpecPlacementTypeReconfigure),
+				ResourcePools:           test.poolMoRefs,
+				PlacementType:           string(types.PlaceVmsXClusterSpecPlacementTypeReconfigure),
+				HostRecommRequired:      types.NewBool(true),
+				DatastoreRecommRequired: types.NewBool(true),
 			}
 
 			placeVmsXClusterSpec.VmPlacementSpecs = []types.PlaceVmsXClusterSpecVmPlacementSpec{{
@@ -863,6 +881,18 @@ func TestPlaceVmsXClusterReconfigure(t *testing.T) {
 				if len(res.PlacementInfos) != len(placeVmsXClusterSpec.VmPlacementSpecs) {
 					t.Errorf("%d PlacementInfos vs %d VmPlacementSpecs", len(res.PlacementInfos), len(placeVmsXClusterSpec.VmPlacementSpecs))
 				}
+
+				for _, pinfo := range res.PlacementInfos {
+					for _, action := range pinfo.Recommendation.Action {
+						if reconfigureAction, ok := action.(*types.ClusterClusterReconfigurePlacementAction); ok {
+							if reconfigureAction.TargetHost == nil {
+								t.Errorf("Test %v: received nil host recommendation", testNo)
+							}
+						} else {
+							t.Errorf("Test %v: received wrong action type in recommendation", testNo)
+						}
+					}
+				}
 			}
 		}
 	}, vpx)