Repository: vmware/govmomi
Pull request #3500 (2024-07-15 19:01:08)

vcsim: add VmCompatibilityChecker and VmProvisioningChecker
govc: add vm.migrate -net flag
govc: add '-spec' flag to vm clone, migrate and create commands
govc: add vm.check commands


Patch:
diff --git a/.github/workflows/govmomi-release.yaml b/.github/workflows/govmomi-release.yaml
index 9e191dd0..ff5b79d9 100644
--- a/.github/workflows/govmomi-release.yaml
+++ b/.github/workflows/govmomi-release.yaml
@@ -72,7 +72,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: '1.20'
+          go-version: '1.22'
 
       - name: Update version.go
         run: |
diff --git a/go.mod b/go.mod
index 6cb237ed..dd799c5f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/vmware/govmomi
 
-go 1.19
+go 1.21
 
 require (
 	github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d
diff --git a/govc/USAGE.md b/govc/USAGE.md
index 87b6d30a..ac6c0eaa 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -6869,6 +6869,10 @@ Options:
   -ds=                       Datastore [GOVC_DATASTORE]
   -folder=                   Inventory folder [GOVC_FOLDER]
   -host=                     Host system [GOVC_HOST]
+  -net=                      Network [GOVC_NETWORK]
+  -net.adapter=e1000         Network adapter type
+  -net.address=              Network hardware address
+  -net.protocol=             Network device protocol. Applicable to vmxnet3vrdma. Default to 'rocev2'
   -pool=                     Resource pool [GOVC_RESOURCE_POOL]
   -priority=defaultPriority  The task priority
   -vm=                       Virtual machine [GOVC_VM]
diff --git a/govc/flags/output.go b/govc/flags/output.go
index 8a2344b4..8975285e 100644
--- a/govc/flags/output.go
+++ b/govc/flags/output.go
@@ -30,6 +30,7 @@ import (
 
 	"github.com/dougm/pretty"
 
+	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/task"
 	"github.com/vmware/govmomi/vim25/progress"
 	"github.com/vmware/govmomi/vim25/soap"
@@ -49,6 +50,7 @@ type OutputFlag struct {
 	TTY  bool
 	Dump bool
 	Out  io.Writer
+	Spec bool
 
 	formatError  bool
 	formatIndent bool
@@ -71,6 +73,9 @@ func (flag *OutputFlag) Register(ctx context.Context, f *flag.FlagSet) {
 		f.BoolVar(&flag.JSON, "json", false, "Enable JSON output")
 		f.BoolVar(&flag.XML, "xml", false, "Enable XML output")
 		f.BoolVar(&flag.Dump, "dump", false, "Enable Go output")
+		if cli.ShowUnreleased() {
+			f.BoolVar(&flag.Spec, "spec", false, "Output spec without sending request")
+		}
 		// Avoid adding more flags for now..
 		flag.formatIndent = os.Getenv("GOVC_INDENT") != "false"      // Default to indented output
 		flag.formatError = os.Getenv("GOVC_FORMAT_ERROR") != "false" // Default to formatted errors
@@ -158,6 +163,25 @@ func dumpValue(val interface{}) interface{} {
 	return val
 }
 
+type outputAny struct {
+	Value any
+}
+
+func (*outputAny) Write(io.Writer) error {
+	return nil
+}
+
+func (a *outputAny) Dump() interface{} {
+	return a.Value
+}
+
+func (flag *OutputFlag) WriteAny(val any) error {
+	if !flag.All() {
+		flag.XML = true
+	}
+	return flag.WriteResult(&outputAny{val})
+}
+
 func (flag *OutputFlag) WriteResult(result OutputWriter) error {
 	var err error
 
diff --git a/govc/main.go b/govc/main.go
index 4943d3c5..bae197e7 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -113,6 +113,7 @@ import (
 	_ "github.com/vmware/govmomi/govc/vlcm/depot/content/baseimages"
 	_ "github.com/vmware/govmomi/govc/vlcm/depot/offline"
 	_ "github.com/vmware/govmomi/govc/vm"
+	_ "github.com/vmware/govmomi/govc/vm/check"
 	_ "github.com/vmware/govmomi/govc/vm/dataset"
 	_ "github.com/vmware/govmomi/govc/vm/dataset/entry"
 	_ "github.com/vmware/govmomi/govc/vm/disk"
diff --git a/govc/vm/check/compat.go b/govc/vm/check/compat.go
new file mode 100644
index 00000000..3702236f
--- /dev/null
+++ b/govc/vm/check/compat.go
@@ -0,0 +1,62 @@
+/*
+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 check
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+)
+
+type compat struct {
+	checkFlag
+}
+
+func init() {
+	cli.Register("vm.check.compat", &compat{}, true)
+}
+
+func (cmd *compat) Description() string {
+	return `Check if VM can be placed on the given HOST in the given resource POOL.
+
+Examples:
+  govc vm.check.compat -vm my-vm -pool $pool`
+}
+
+func (cmd *compat) Run(ctx context.Context, f *flag.FlagSet) error {
+	vm, err := cmd.VirtualMachine()
+	if err != nil {
+		return err
+	}
+
+	if vm == nil {
+		return flag.ErrHelp
+	}
+
+	checker, err := cmd.compatChecker()
+	if err != nil {
+		return err
+	}
+
+	res, err := checker.CheckCompatibility(ctx, vm.Reference(), cmd.Host, cmd.Pool, cmd.Test...)
+	if err != nil {
+		return err
+	}
+
+	return cmd.result(ctx, res)
+}
diff --git a/govc/vm/check/config.go b/govc/vm/check/config.go
new file mode 100644
index 00000000..2c53a5e5
--- /dev/null
+++ b/govc/vm/check/config.go
@@ -0,0 +1,60 @@
+/*
+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 check
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type config struct {
+	checkFlag
+}
+
+func init() {
+	cli.Register("vm.check.config", &config{}, true)
+}
+
+func (cmd *config) Description() string {
+	return `Check if VM config spec can be applied.
+
+Examples:
+  govc vm.create -spec ... | govc vm.check.config -pool $pool`
+}
+
+func (cmd *config) Run(ctx context.Context, f *flag.FlagSet) error {
+	var spec types.VirtualMachineConfigSpec
+
+	if err := cmd.Spec(&spec); err != nil {
+		return err
+	}
+
+	checker, err := cmd.compatChecker()
+	if err != nil {
+		return err
+	}
+
+	res, err := checker.CheckVmConfig(ctx, spec, cmd.Machine, cmd.Host, cmd.Pool, cmd.Test...)
+	if err != nil {
+		return err
+	}
+
+	return cmd.result(ctx, res)
+}
diff --git a/govc/vm/check/flag.go b/govc/vm/check/flag.go
new file mode 100644
index 00000000..079c3b07
--- /dev/null
+++ b/govc/vm/check/flag.go
@@ -0,0 +1,167 @@
+/*
+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 check
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"slices"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/vim25/types"
+	"github.com/vmware/govmomi/vim25/xml"
+)
+
+type checkFlag struct {
+	*flags.VirtualMachineFlag
+	*flags.HostSystemFlag
+	*flags.ResourcePoolFlag
+
+	Machine, Host, Pool *types.ManagedObjectReference
+
+	Test []string
+}
+
+func (cmd *checkFlag) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	cmd.HostSystemFlag, ctx = flags.NewHostSystemFlag(ctx)
+	cmd.HostSystemFlag.Register(ctx, f)
+	cmd.ResourcePoolFlag, ctx = flags.NewResourcePoolFlag(ctx)
+	cmd.ResourcePoolFlag.Register(ctx, f)
+}
+
+func (cmd *checkFlag) Process(ctx context.Context) error {
+	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.HostSystemFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.ResourcePoolFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	vm, err := cmd.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm != nil {
+		cmd.Machine = types.NewReference(vm.Reference())
+	}
+
+	host, err := cmd.HostSystemIfSpecified()
+	if err != nil {
+		return err
+	}
+	if host != nil {
+		cmd.Host = types.NewReference(host.Reference())
+	}
+
+	pool, err := cmd.ResourcePoolIfSpecified()
+	if err != nil {
+		return err
+	}
+	if pool != nil {
+		cmd.Pool = types.NewReference(pool.Reference())
+	}
+
+	return nil
+}
+
+func (cmd *checkFlag) provChecker() (*object.VmProvisioningChecker, error) {
+	c, err := cmd.VirtualMachineFlag.Client()
+	if err != nil {
+		return nil, err
+	}
+
+	return object.NewVmProvisioningChecker(c), nil
+}
+
+func (cmd *checkFlag) compatChecker() (*object.VmCompatibilityChecker, error) {
+	c, err := cmd.VirtualMachineFlag.Client()
+	if err != nil {
+		return nil, err
+	}
+
+	return object.NewVmCompatibilityChecker(c), nil
+}
+
+func (cmd *checkFlag) Spec(spec any) error {
+	dec := xml.NewDecoder(os.Stdin)
+	dec.TypeFunc = types.TypeFunc()
+	return dec.Decode(spec)
+}
+
+// return cmd.VirtualMachineFlag.WriteResult(&checkResult{res, ctx, cmd.VirtualMachineFlag})
+func (cmd *checkFlag) result(ctx context.Context, res []types.CheckResult) error {
+	return cmd.VirtualMachineFlag.WriteResult(&checkResult{res, ctx, cmd.VirtualMachineFlag})
+}
+
+type checkResult struct {
+	Result []types.CheckResult `json:"result"`
+	ctx    context.Context
+	vm     *flags.VirtualMachineFlag
+}
+
+func (res *checkResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	c, err := res.vm.Client()
+	if err != nil {
+		return err
+	}
+
+	for _, r := range res.Result {
+		fields := []struct {
+			name  string
+			moid  *types.ManagedObjectReference
+			fault []types.LocalizedMethodFault
+		}{
+			{"VM", r.Vm, nil},
+			{"Host", r.Host, nil},
+			{"Warning", nil, r.Warning},
+			{"Error", nil, r.Error},
+		}
+
+		for _, f := range fields {
+			var val string
+			if f.moid == nil {
+				var msgs []string
+				for _, m := range f.fault {
+					msgs = append(msgs, m.LocalizedMessage)
+				}
+				val = strings.Join(slices.Compact(msgs), "\n\t")
+			} else {
+				val, err = find.InventoryPath(res.ctx, c, *f.moid)
+				if err != nil {
+					return err
+				}
+			}
+			fmt.Fprintf(tw, "%s:\t%s\n", f.name, val)
+		}
+	}
+
+	return tw.Flush()
+}
diff --git a/govc/vm/check/relocate.go b/govc/vm/check/relocate.go
new file mode 100644
index 00000000..a33cfa5b
--- /dev/null
+++ b/govc/vm/check/relocate.go
@@ -0,0 +1,68 @@
+/*
+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 check
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type relocate struct {
+	checkFlag
+}
+
+func init() {
+	cli.Register("vm.check.relocate", &relocate{}, true)
+}
+
+func (cmd *relocate) Description() string {
+	return `Check if VM can be relocated.
+
+Examples:
+  govc vm.migrate -spec my-vm | govc vm.check.relocate -vm my-vm`
+}
+
+func (cmd *relocate) Run(ctx context.Context, f *flag.FlagSet) error {
+	vm, err := cmd.VirtualMachine()
+	if err != nil {
+		return err
+	}
+
+	if vm == nil {
+		return flag.ErrHelp
+	}
+
+	var spec types.VirtualMachineRelocateSpec
+	if err := cmd.Spec(&spec); err != nil {
+		return err
+	}
+
+	checker, err := cmd.provChecker()
+	if err != nil {
+		return err
+	}
+
+	res, err := checker.CheckRelocate(ctx, vm.Reference(), spec, cmd.Test...)
+	if err != nil {
+		return err
+	}
+
+	return cmd.result(ctx, res)
+}
diff --git a/govc/vm/clone.go b/govc/vm/clone.go
index d9ef49a2..0e85970e 100644
--- a/govc/vm/clone.go
+++ b/govc/vm/clone.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2014-2016 VMware, Inc. All Rights Reserved.
+Copyright (c) 2016-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
+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,
@@ -243,6 +243,9 @@ func (cmd *clone) Run(ctx context.Context, f *flag.FlagSet) error {
 	if err != nil {
 		return err
 	}
+	if cmd.Spec {
+		return nil
+	}
 
 	if cmd.cpus > 0 || cmd.memory > 0 || cmd.annotation != "" {
 		vmConfigSpec := types.VirtualMachineConfigSpec{}
@@ -471,6 +474,10 @@ func (cmd *clone) cloneVM(ctx context.Context) (*object.VirtualMachine, error) {
 		cloneSpec.Customization = &customSpec
 	}
 
+	if cmd.Spec {
+		return nil, cmd.WriteAny(cloneSpec)
+	}
+
 	task, err := cmd.VirtualMachine.Clone(ctx, cmd.Folder, cmd.name, *cloneSpec)
 	if err != nil {
 		return nil, err
diff --git a/govc/vm/create.go b/govc/vm/create.go
index 46c3a57d..84950dad 100644
--- a/govc/vm/create.go
+++ b/govc/vm/create.go
@@ -307,7 +307,7 @@ func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
 	if err != nil {
 		return err
 	}
-	if cmd.place {
+	if cmd.place || cmd.Spec {
 		return nil
 	}
 	info, err := task.WaitForResult(ctx, nil)
@@ -490,7 +490,7 @@ func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 		return nil, fmt.Errorf("please provide either a cluster, datastore or datastore-cluster")
 	}
 
-	if !cmd.force {
+	if !cmd.force && !cmd.Spec {
 		vmxPath := fmt.Sprintf("%s/%s.vmx", cmd.name, cmd.name)
 
 		_, err := datastore.Stat(ctx, vmxPath)
@@ -506,6 +506,10 @@ func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 		VmPathName: fmt.Sprintf("[%s]", datastore.Name()),
 	}
 
+	if cmd.Spec {
+		return nil, cmd.WriteAny(spec)
+	}
+
 	return folder.CreateVM(ctx, *spec, cmd.ResourcePool, cmd.HostSystem)
 }
 
diff --git a/govc/vm/migrate.go b/govc/vm/migrate.go
index cf133e86..0e6d1a9f 100644
--- a/govc/vm/migrate.go
+++ b/govc/vm/migrate.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2016 VMware, Inc. All Rights Reserved.
+Copyright (c) 2016-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
+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,
@@ -32,6 +32,7 @@ type migrate struct {
 	*flags.ResourcePoolFlag
 	*flags.HostSystemFlag
 	*flags.DatastoreFlag
+	*flags.NetworkFlag
 	*flags.VirtualMachineFlag
 
 	priority types.VirtualMachineMovePriority
@@ -58,6 +59,9 @@ func (cmd *migrate) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx)
 	cmd.DatastoreFlag.Register(ctx, f)
 
+	cmd.NetworkFlag, ctx = flags.NewNetworkFlag(ctx)
+	cmd.NetworkFlag.Register(ctx, f)
+
 	f.StringVar((*string)(&cmd.priority), "priority", string(types.VirtualMachineMovePriorityDefaultPriority), "The task priority")
 }
 
@@ -77,6 +81,9 @@ func (cmd *migrate) Process(ctx context.Context) error {
 	if err := cmd.DatastoreFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.NetworkFlag.Process(ctx); err != nil {
+		return err
+	}
 
 	return nil
 }
@@ -95,7 +102,36 @@ Examples:
 }
 
 func (cmd *migrate) relocate(ctx context.Context, vm *object.VirtualMachine) error {
-	task, err := vm.Relocate(ctx, cmd.spec, cmd.priority)
+	spec := cmd.spec
+
+	if cmd.NetworkFlag.IsSet() {
+		dev, err := cmd.NetworkFlag.Device()
+		if err != nil {
+			return err
+		}
+
+		devices, err := vm.Device(ctx)
+		if err != nil {
+			return err
+		}
+
+		net := devices.SelectByType((*types.VirtualEthernetCard)(nil))
+		if len(net) != 1 {
+			return fmt.Errorf("-net specified, but %s has %d nics", vm.Name(), len(net))
+		}
+		cmd.NetworkFlag.Change(net[0], dev)
+
+		spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{
+			Device:    net[0],
+			Operation: types.VirtualDeviceConfigSpecOperationEdit,
+		})
+	}
+
+	if cmd.VirtualMachineFlag.Spec {
+		return cmd.VirtualMachineFlag.WriteAny(spec)
+	}
+
+	task, err := vm.Relocate(ctx, spec, cmd.priority)
 	if err != nil {
 		return err
 	}
diff --git a/object/vm_compatability_checker.go b/object/vm_compatability_checker.go
new file mode 100644
index 00000000..678b535d
--- /dev/null
+++ b/object/vm_compatability_checker.go
@@ -0,0 +1,99 @@
+/*
+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 object
+
+import (
+	"context"
+
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+// VmCompatibilityChecker models the CompatibilityChecker, a singleton managed
+// object that can answer questions about compatibility of a virtual machine
+// with a host.
+//
+// For more information, see:
+// https://dp-downloads.broadcom.com/api-content/apis/API_VWSA_001/8.0U3/html/ReferenceGuides/vim.vm.check.CompatibilityChecker.html
+type VmCompatibilityChecker struct {
+	Common
+}
+
+func NewVmCompatibilityChecker(c *vim25.Client) *VmCompatibilityChecker {
+	return &VmCompatibilityChecker{
+		Common: NewCommon(c, *c.ServiceContent.VmCompatibilityChecker),
+	}
+}
+
+func (c VmCompatibilityChecker) CheckCompatibility(
+	ctx context.Context,
+	vm types.ManagedObjectReference,
+	host *types.ManagedObjectReference,
+	pool *types.ManagedObjectReference,
+	testTypes ...string) ([]types.CheckResult, error) {
+
+	req := types.CheckCompatibility_Task{
+		This:     c.Reference(),
+		Vm:       vm,
+		Host:     host,
+		Pool:     pool,
+		TestType: testTypes,
+	}
+
+	res, err := methods.CheckCompatibility_Task(ctx, c.c, &req)
+	if err != nil {
+		return nil, err
+	}
+
+	ti, err := NewTask(c.c, res.Returnval).WaitForResult(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return ti.Result.(types.ArrayOfCheckResult).CheckResult, nil
+}
+
+func (c VmCompatibilityChecker) CheckVmConfig(
+	ctx context.Context,
+	spec types.VirtualMachineConfigSpec,
+	vm *types.ManagedObjectReference,
+	host *types.ManagedObjectReference,
+	pool *types.ManagedObjectReference,
+	testTypes ...string) ([]types.CheckResult, error) {
+
+	req := types.CheckVmConfig_Task{
+		This:     c.Reference(),
+		Spec:     spec,
+		Vm:       vm,
+		Host:     host,
+		Pool:     pool,
+		TestType: testTypes,
+	}
+
+	res, err := methods.CheckVmConfig_Task(ctx, c.c, &req)
+	if err != nil {
+		return nil, err
+	}
+
+	ti, err := NewTask(c.c, res.Returnval).WaitForResult(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return ti.Result.(types.ArrayOfCheckResult).CheckResult, nil
+}
diff --git a/object/vm_provisioning_checker.go b/object/vm_provisioning_checker.go
new file mode 100644
index 00000000..632f696c
--- /dev/null
+++ b/object/vm_provisioning_checker.go
@@ -0,0 +1,67 @@
+/*
+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 object
+
+import (
+	"context"
+
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+// VmProvisioningChecker models the ProvisioningChecker, a singleton managed
+// object that can answer questions about the feasibility of certain
+// provisioning operations.
+//
+// For more information, see:
+// https://dp-downloads.broadcom.com/api-content/apis/API_VWSA_001/8.0U3/html/ReferenceGuides/vim.vm.check.ProvisioningChecker.html
+type VmProvisioningChecker struct {
+	Common
+}
+
+func NewVmProvisioningChecker(c *vim25.Client) *VmProvisioningChecker {
+	return &VmProvisioningChecker{
+		Common: NewCommon(c, *c.ServiceContent.VmProvisioningChecker),
+	}
+}
+
+func (c VmProvisioningChecker) CheckRelocate(
+	ctx context.Context,
+	vm types.ManagedObjectReference,
+	spec types.VirtualMachineRelocateSpec,
+	testTypes ...string) ([]types.CheckResult, error) {
+
+	req := types.CheckRelocate_Task{
+		This:     c.Reference(),
+		Vm:       vm,
+		Spec:     spec,
+		TestType: testTypes,
+	}
+
+	res, err := methods.CheckRelocate_Task(ctx, c.c, &req)
+	if err != nil {
+		return nil, err
+	}
+
+	ti, err := NewTask(c.c, res.Returnval).WaitForResult(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return ti.Result.(types.ArrayOfCheckResult).CheckResult, nil
+}
diff --git a/simulator/esx/host_capability.go b/simulator/esx/host_capability.go
new file mode 100644
index 00000000..d407cb6d
--- /dev/null
+++ b/simulator/esx/host_capability.go
@@ -0,0 +1,24 @@
+/*
+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 esx
+
+import "github.com/vmware/govmomi/vim25/types"
+
+// HostCapability captured via `govc object.collect -dump $host capability`
+var HostCapability = &types.HostCapability{
+	MaxSupportedVmMemory: 25149440, // 25TB since 7.0U1
+}
diff --git a/simulator/host_system.go b/simulator/host_system.go
index 367491a6..2640e18e 100644
--- a/simulator/host_system.go
+++ b/simulator/host_system.go
@@ -77,6 +77,10 @@ func NewHostSystem(host mo.HostSystem) *HostSystem {
 		info := *esx.HostHardwareInfo
 		hs.Hardware = &info
 	}
+	if hs.Capability == nil {
+		capability := *esx.HostCapability
+		hs.Capability = &capability
+	}
 
 	cfg := new(types.HostConfigInfo)
 	deepCopy(hs.Config, cfg)
diff --git a/simulator/model.go b/simulator/model.go
index e80d0f30..3a69e6ac 100644
--- a/simulator/model.go
+++ b/simulator/model.go
@@ -232,46 +232,48 @@ func (*Model) fmtName(prefix string, num int) string {
 
 // kinds maps managed object types to their vcsim wrapper types
 var kinds = map[string]reflect.Type{
-	"AuthorizationManager":            reflect.TypeOf((*AuthorizationManager)(nil)).Elem(),
-	"ClusterComputeResource":          reflect.TypeOf((*ClusterComputeResource)(nil)).Elem(),
-	"CustomFieldsManager":             reflect.TypeOf((*CustomFieldsManager)(nil)).Elem(),
-	"CustomizationSpecManager":        reflect.TypeOf((*CustomizationSpecManager)(nil)).Elem(),
-	"Datacenter":                      reflect.TypeOf((*Datacenter)(nil)).Elem(),
-	"Datastore":                       reflect.TypeOf((*Datastore)(nil)).Elem(),
-	"DistributedVirtualPortgroup":     reflect.TypeOf((*DistributedVirtualPortgroup)(nil)).Elem(),
-	"DistributedVirtualSwitch":        reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
-	"DistributedVirtualSwitchManager": reflect.TypeOf((*DistributedVirtualSwitchManager)(nil)).Elem(),
-	"EnvironmentBrowser":              reflect.TypeOf((*EnvironmentBrowser)(nil)).Elem(),
-	"EventManager":                    reflect.TypeOf((*EventManager)(nil)).Elem(),
-	"ExtensionManager":                reflect.TypeOf((*ExtensionManager)(nil)).Elem(),
-	"FileManager":                     reflect.TypeOf((*FileManager)(nil)).Elem(),
-	"Folder":                          reflect.TypeOf((*Folder)(nil)).Elem(),
-	"GuestOperationsManager":          reflect.TypeOf((*GuestOperationsManager)(nil)).Elem(),
-	"HostDatastoreBrowser":            reflect.TypeOf((*HostDatastoreBrowser)(nil)).Elem(),
-	"HostLocalAccountManager":         reflect.TypeOf((*HostLocalAccountManager)(nil)).Elem(),
-	"HostNetworkSystem":               reflect.TypeOf((*HostNetworkSystem)(nil)).Elem(),
-	"HostCertificateManager":          reflect.TypeOf((*HostCertificateManager)(nil)).Elem(),
-	"HostSystem":                      reflect.TypeOf((*HostSystem)(nil)).Elem(),
-	"IpPoolManager":                   reflect.TypeOf((*IpPoolManager)(nil)).Elem(),
-	"LicenseManager":                  reflect.TypeOf((*LicenseManager)(nil)).Elem(),
-	"OptionManager":                   reflect.TypeOf((*OptionManager)(nil)).Elem(),
-	"OvfManager":                      reflect.TypeOf((*OvfManager)(nil)).Elem(),
-	"PerformanceManager":              reflect.TypeOf((*PerformanceManager)(nil)).Elem(),
-	"PropertyCollector":               reflect.TypeOf((*PropertyCollector)(nil)).Elem(),
-	"ResourcePool":                    reflect.TypeOf((*ResourcePool)(nil)).Elem(),
-	"SearchIndex":                     reflect.TypeOf((*SearchIndex)(nil)).Elem(),
-	"SessionManager":                  reflect.TypeOf((*SessionManager)(nil)).Elem(),
-	"StoragePod":                      reflect.TypeOf((*StoragePod)(nil)).Elem(),
-	"StorageResourceManager":          reflect.TypeOf((*StorageResourceManager)(nil)).Elem(),
-	"TaskManager":                     reflect.TypeOf((*TaskManager)(nil)).Elem(),
-	"TenantTenantManager":             reflect.TypeOf((*TenantManager)(nil)).Elem(),
-	"UserDirectory":                   reflect.TypeOf((*UserDirectory)(nil)).Elem(),
-	"VcenterVStorageObjectManager":    reflect.TypeOf((*VcenterVStorageObjectManager)(nil)).Elem(),
-	"ViewManager":                     reflect.TypeOf((*ViewManager)(nil)).Elem(),
-	"VirtualApp":                      reflect.TypeOf((*VirtualApp)(nil)).Elem(),
-	"VirtualDiskManager":              reflect.TypeOf((*VirtualDiskManager)(nil)).Elem(),
-	"VirtualMachine":                  reflect.TypeOf((*VirtualMachine)(nil)).Elem(),
-	"VmwareDistributedVirtualSwitch":  reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
+	"AuthorizationManager":               reflect.TypeOf((*AuthorizationManager)(nil)).Elem(),
+	"ClusterComputeResource":             reflect.TypeOf((*ClusterComputeResource)(nil)).Elem(),
+	"CustomFieldsManager":                reflect.TypeOf((*CustomFieldsManager)(nil)).Elem(),
+	"CustomizationSpecManager":           reflect.TypeOf((*CustomizationSpecManager)(nil)).Elem(),
+	"Datacenter":                         reflect.TypeOf((*Datacenter)(nil)).Elem(),
+	"Datastore":                          reflect.TypeOf((*Datastore)(nil)).Elem(),
+	"DistributedVirtualPortgroup":        reflect.TypeOf((*DistributedVirtualPortgroup)(nil)).Elem(),
+	"DistributedVirtualSwitch":           reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
+	"DistributedVirtualSwitchManager":    reflect.TypeOf((*DistributedVirtualSwitchManager)(nil)).Elem(),
+	"EnvironmentBrowser":                 reflect.TypeOf((*EnvironmentBrowser)(nil)).Elem(),
+	"EventManager":                       reflect.TypeOf((*EventManager)(nil)).Elem(),
+	"ExtensionManager":                   reflect.TypeOf((*ExtensionManager)(nil)).Elem(),
+	"FileManager":                        reflect.TypeOf((*FileManager)(nil)).Elem(),
+	"Folder":                             reflect.TypeOf((*Folder)(nil)).Elem(),
+	"GuestOperationsManager":             reflect.TypeOf((*GuestOperationsManager)(nil)).Elem(),
+	"HostDatastoreBrowser":               reflect.TypeOf((*HostDatastoreBrowser)(nil)).Elem(),
+	"HostLocalAccountManager":            reflect.TypeOf((*HostLocalAccountManager)(nil)).Elem(),
+	"HostNetworkSystem":                  reflect.TypeOf((*HostNetworkSystem)(nil)).Elem(),
+	"HostCertificateManager":             reflect.TypeOf((*HostCertificateManager)(nil)).Elem(),
+	"HostSystem":                         reflect.TypeOf((*HostSystem)(nil)).Elem(),
+	"IpPoolManager":                      reflect.TypeOf((*IpPoolManager)(nil)).Elem(),
+	"LicenseManager":                     reflect.TypeOf((*LicenseManager)(nil)).Elem(),
+	"OptionManager":                      reflect.TypeOf((*OptionManager)(nil)).Elem(),
+	"OvfManager":                         reflect.TypeOf((*OvfManager)(nil)).Elem(),
+	"PerformanceManager":                 reflect.TypeOf((*PerformanceManager)(nil)).Elem(),
+	"PropertyCollector":                  reflect.TypeOf((*PropertyCollector)(nil)).Elem(),
+	"ResourcePool":                       reflect.TypeOf((*ResourcePool)(nil)).Elem(),
+	"SearchIndex":                        reflect.TypeOf((*SearchIndex)(nil)).Elem(),
+	"SessionManager":                     reflect.TypeOf((*SessionManager)(nil)).Elem(),
+	"StoragePod":                         reflect.TypeOf((*StoragePod)(nil)).Elem(),
+	"StorageResourceManager":             reflect.TypeOf((*StorageResourceManager)(nil)).Elem(),
+	"TaskManager":                        reflect.TypeOf((*TaskManager)(nil)).Elem(),
+	"TenantTenantManager":                reflect.TypeOf((*TenantManager)(nil)).Elem(),
+	"UserDirectory":                      reflect.TypeOf((*UserDirectory)(nil)).Elem(),
+	"VcenterVStorageObjectManager":       reflect.TypeOf((*VcenterVStorageObjectManager)(nil)).Elem(),
+	"ViewManager":                        reflect.TypeOf((*ViewManager)(nil)).Elem(),
+	"VirtualApp":                         reflect.TypeOf((*VirtualApp)(nil)).Elem(),
+	"VirtualDiskManager":                 reflect.TypeOf((*VirtualDiskManager)(nil)).Elem(),
+	"VirtualMachine":                     reflect.TypeOf((*VirtualMachine)(nil)).Elem(),
+	"VirtualMachineCompatibilityChecker": reflect.TypeOf((*VmCompatibilityChecker)(nil)).Elem(),
+	"VirtualMachineProvisioningChecker":  reflect.TypeOf((*VmProvisioningChecker)(nil)).Elem(),
+	"VmwareDistributedVirtualSwitch":     reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
 }
 
 func loadObject(content types.ObjectContent) (mo.Reference, error) {
diff --git a/simulator/registry.go b/simulator/registry.go
index 7c3c7776..acfc2265 100644
--- a/simulator/registry.go
+++ b/simulator/registry.go
@@ -572,6 +572,16 @@ func (r *Registry) TenantManager() *TenantManager {
 	return r.Get(r.content().TenantManager.Reference()).(*TenantManager)
 }
 
+// VmCompatibilityChecker returns VmCompatibilityChecker singleton
+func (r *Registry) VmCompatibilityChecker() *VmCompatibilityChecker {
+	return r.Get(r.content().VmCompatibilityChecker.Reference()).(*VmCompatibilityChecker)
+}
+
+// VmProvisioningChecker returns VmProvisioningChecker singleton
+func (r *Registry) VmProvisioningChecker() *VmProvisioningChecker {
+	return r.Get(r.content().VmProvisioningChecker.Reference()).(*VmProvisioningChecker)
+}
+
 // ExtensionManager returns the ExtensionManager singleton
 func (r *Registry) ExtensionManager() *ExtensionManager {
 	return r.Get(r.content().ExtensionManager.Reference()).(*ExtensionManager)
diff --git a/simulator/resource_pool.go b/simulator/resource_pool.go
index d34fa5b8..0ecd6f6f 100644
--- a/simulator/resource_pool.go
+++ b/simulator/resource_pool.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-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.
@@ -39,6 +39,17 @@ func asResourcePoolMO(obj mo.Reference) (*mo.ResourcePool, bool) {
 	return rp, ok
 }
 
+func resourcePoolHosts(ctx *Context, pool *ResourcePool) []types.ManagedObjectReference {
+	switch owner := ctx.Map.Get(pool.Owner).(type) {
+	case *ClusterComputeResource:
+		return owner.Host
+	case *mo.ComputeResource:
+		return owner.Host
+	default:
+		return nil
+	}
+}
+
 func NewResourcePool() *ResourcePool {
 	pool := &ResourcePool{
 		ResourcePool: esx.ResourcePool,
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 1a05352e..c026070c 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -2345,6 +2345,11 @@ func (vm *VirtualMachine) RelocateVMTask(ctx *Context, req *types.RelocateVM_Tas
 			})
 		}
 
+		cspec := &types.VirtualMachineConfigSpec{DeviceChange: req.Spec.DeviceChange}
+		if err := vm.configureDevices(ctx, cspec); err != nil {
+			return nil, err
+		}
+
 		ctx.postEvent(&types.VmMigratedEvent{
 			VmEvent:          vm.event(),
 			SourceHost:       *ctx.Map.Get(*vm.Runtime.Host).(*HostSystem).eventArgument(),
diff --git a/simulator/vm_compatibility_checker.go b/simulator/vm_compatibility_checker.go
new file mode 100644
index 00000000..398199d3
--- /dev/null
+++ b/simulator/vm_compatibility_checker.go
@@ -0,0 +1,188 @@
+/*
+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 simulator
+
+import (
+	"fmt"
+	"math/rand"
+	"slices"
+
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/soap"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type VmCompatibilityChecker struct {
+	mo.VirtualMachineCompatibilityChecker
+}
+
+func resolveHostsAndPool(ctx *Context, vm, host, pool *types.ManagedObjectReference) (*ResourcePool, []types.ManagedObjectReference) {
+	var vmMo *VirtualMachine
+	var poolMo *ResourcePool
+
+	switch {
+	case pool != nil:
+		poolMo = ctx.Map.Get(*pool).(*ResourcePool)
+	case vm != nil:
+		vmMo = ctx.Map.Get(*vm).(*VirtualMachine)
+		poolMo = ctx.Map.Get(*vmMo.ResourcePool).(*ResourcePool)
+	case host != nil:
+		h := ctx.Map.Get(*host).(*HostSystem)
+		parent := hostParent(&h.HostSystem).ResourcePool
+		poolMo = ctx.Map.Get(*parent).(*ResourcePool)
+	}
+
+	var hosts []types.ManagedObjectReference
+
+	switch {
+	case host != nil:
+		hosts = append(hosts, *host)
+	case pool != nil:
+		hosts = resourcePoolHosts(ctx, poolMo)
+	case vm != nil:
+		hosts = append(hosts, *vmMo.Runtime.Host)
+	}
+
+	return poolMo, hosts
+}
+
+func validateHostsAndPool(ctx *Context, pool *ResourcePool, hosts []types.ManagedObjectReference) *types.InvalidArgument {
+	allHosts := resourcePoolHosts(ctx, pool)
+
+	for _, host := range hosts {
+		if !slices.Contains(allHosts, host) {
+			return &types.InvalidArgument{
+				InvalidProperty: "spec.pool",
+			}
+		}
+	}
+
+	return nil
+}
+
+func (c *VmCompatibilityChecker) checkVmConfigSpec(ctx *Context, check *types.CheckResult, spec types.VirtualMachineConfigSpec, hosts []types.ManagedObjectReference) {
+	if check.Host == nil {
+		// By default all hosts use the same HostSystem template, so we check against any.
+		// But we could choose a host based on the spec, e.g. record + playback of real hosts
+		check.Host = &hosts[rand.Intn(len(hosts))]
+	}
+
+	host := ctx.Map.Get(*check.Host).(*HostSystem)
+
+	mem := int32(spec.MemoryMB)
+	if mem > 0 {
+		min := int32(4)
+		max := host.Capability.MaxSupportedVmMemory
+		if mem > max || mem < min {
+			check.Warning = append(check.Warning, types.LocalizedMethodFault{
+				Fault: &types.MemorySizeNotSupported{
+					MemorySizeMB:    mem,
+					MinMemorySizeMB: min,
+					MaxMemorySizeMB: max,
+				},
+				LocalizedMessage: fmt.Sprintf("vm requires %d MB of memory, outside the range of %d to %d", mem, min, max),
+			})
+		}
+	}
+
+	cpu := spec.NumCPUs
+	if cpu > 0 {
+		max := int32(host.Summary.Hardware.NumCpuCores)
+		if cpu > max {
+			check.Warning = append(check.Warning, types.LocalizedMethodFault{
+				Fault: &types.NotEnoughCpus{
+					NumCpuDest: max,
+					NumCpuVm:   cpu,
+				},
+				LocalizedMessage: fmt.Sprintf("vm requires %d CPUs, host has %d", cpu, max),
+			})
+		}
+	}
+
+	if spec.GuestId != "" {
+		var guest types.VirtualMachineGuestOsIdentifier
+		if !slices.Contains(guest.Strings(), spec.GuestId) {
+			check.Warning = append(check.Warning, types.LocalizedMethodFault{
+				Fault: &types.UnsupportedGuest{
+					UnsupportedGuestOS: spec.GuestId,
+				},
+				LocalizedMessage: fmt.Sprintf("vm guest os %s not supported", spec.GuestId),
+			})
+		}
+	}
+}
+
+func (c *VmCompatibilityChecker) CheckVmConfigTask(
+	ctx *Context,
+	r *types.CheckVmConfig_Task) soap.HasFault {
+
+	task := CreateTask(c, "checkVmConfig", func(t *Task) (types.AnyType, types.BaseMethodFault) {
+		if r.Vm == nil && r.Host == nil && r.Pool == nil {
+			return nil, new(types.InvalidArgument)
+		}
+
+		poolMo, hosts := resolveHostsAndPool(ctx, r.Vm, r.Host, r.Pool)
+		if err := validateHostsAndPool(ctx, poolMo, hosts); err != nil {
+			return nil, err
+		}
+
+		check := types.CheckResult{
+			Vm:   r.Vm,
+			Host: r.Host,
+		}
+
+		c.checkVmConfigSpec(ctx, &check, r.Spec, hosts)
+
+		return types.ArrayOfCheckResult{
+			CheckResult: []types.CheckResult{check},
+		}, nil
+	})
+
+	return &methods.CheckVmConfig_TaskBody{
+		Res: &types.CheckVmConfig_TaskResponse{
+			Returnval: task.Run(ctx),
+		},
+	}
+}
+
+func (c *VmCompatibilityChecker) CheckCompatibilityTask(
+	ctx *Context,
+	r *types.CheckCompatibility_Task) soap.HasFault {
+
+	task := CreateTask(c, "checkCompatibility", func(t *Task) (types.AnyType, types.BaseMethodFault) {
+		poolMo, hosts := resolveHostsAndPool(ctx, &r.Vm, r.Host, r.Pool)
+		if err := validateHostsAndPool(ctx, poolMo, hosts); err != nil {
+			return nil, err
+		}
+
+		check := types.CheckResult{
+			Vm:   &r.Vm,
+			Host: r.Host,
+		}
+
+		return types.ArrayOfCheckResult{
+			CheckResult: []types.CheckResult{check},
+		}, nil
+	})
+
+	return &methods.CheckCompatibility_TaskBody{
+		Res: &types.CheckCompatibility_TaskResponse{
+			Returnval: task.Run(ctx),
+		},
+	}
+}
diff --git a/simulator/vm_provisioning_checker.go b/simulator/vm_provisioning_checker.go
new file mode 100644
index 00000000..7634c7d3
--- /dev/null
+++ b/simulator/vm_provisioning_checker.go
@@ -0,0 +1,49 @@
+/*
+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 simulator
+
+import (
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/soap"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type VmProvisioningChecker struct {
+	mo.VirtualMachineProvisioningChecker
+}
+
+func (c *VmProvisioningChecker) CheckRelocateTask(
+	ctx *Context,
+	r *types.CheckRelocate_Task) soap.HasFault {
+
+	task := CreateTask(c, "checkRelocate", func(t *Task) (types.AnyType, types.BaseMethodFault) {
+		check := types.CheckResult{
+			Vm: &r.Vm,
+		}
+
+		return types.ArrayOfCheckResult{
+			CheckResult: []types.CheckResult{check},
+		}, nil
+	})
+
+	return &methods.CheckRelocate_TaskBody{
+		Res: &types.CheckRelocate_TaskResponse{
+			Returnval: task.Run(ctx),
+		},
+	}
+}


Test patch:
diff --git a/.github/workflows/govmomi-govc-tests.yaml b/.github/workflows/govmomi-govc-tests.yaml
index f7a27c46..8569681b 100644
--- a/.github/workflows/govmomi-govc-tests.yaml
+++ b/.github/workflows/govmomi-govc-tests.yaml
@@ -31,7 +31,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        go-version: ["1.20"]
+        go-version: ["1.21"]
         platform: ["ubuntu-20.04"]
         cmd: ["govc-test"]
         experimental: [false]
@@ -73,7 +73,7 @@ jobs:
     name: Verify govc docs are up2date
     strategy:
       matrix:
-        go-version: ["1.20"]
+        go-version: ["1.21"]
         platform: ["ubuntu-20.04"]
     runs-on: ${{ matrix.platform }}
     timeout-minutes: 3
diff --git a/govc/test/vm.bats b/govc/test/vm.bats
index 85c0576d..da01c59b 100755
--- a/govc/test/vm.bats
+++ b/govc/test/vm.bats
@@ -959,6 +959,15 @@ load test_helper
 
   run govc object.collect -s "vm/new-folder/$vm" parent
   assert_success
+
+  run govc vm.info -r "$vm"
+  assert_matches "Network: +DC0_DVPG0"
+
+  run govc vm.migrate -host "$host0" -net "VM Network" "$vm"
+  assert_success
+
+  run govc vm.info -r "$vm"
+  assert_matches "Network: +VM Network"
 }
 
 @test "object name with slash" {
@@ -1232,3 +1241,86 @@ load test_helper
   run govc vm.customize -vm DC0_H0_VM0 -ip 10.0.0.45 -netmask 255.255.0.0 -type Linux
   assert_success
 }
+
+@test "vm.check.config" {
+  vcsim_env -cluster 2
+
+  export GOVC_SHOW_UNRELEASED=true
+
+  vm=DC0_C0_RP0_VM0
+  run govc vm.create -spec -pool DC0_C0/Resources $vm
+  assert_success
+  spec="$output"
+
+  run govc vm.check.config -vm $vm <<<"$spec"
+  assert_success
+
+  # we can use any host in the VM's cluster
+  for host in $(govc find /DC0/host/DC0_C0 -type f) ; do
+    run govc vm.check.config -host "$host" <<<"$spec"
+    assert_success
+  done
+
+  run govc vm.check.config -host DC0_C1_H0 <<<"$spec"
+  assert_failure # pool and host do not belong to the same compute resource
+
+  run govc vm.check.config -pool DC0_C1/Resources <<<"$spec"
+  assert_failure # pool and host do not belong to the same compute resource
+
+  # spec.memoryMB
+  max_mem=$(govc object.collect -s DC0_C1_H0 capability.maxSupportedVmMemory)
+  run govc vm.create -spec -m "$((max_mem+100))" $vm
+  assert_success
+  spec="$output"
+
+  run govc vm.check.config -vm $vm -json <<<"$spec"
+  assert_success
+  assert_matches "outside the range"
+
+  # spec.numCPUs
+  max_cpu=$(govc object.collect -s "$host" summary.hardware.numCpuCores)
+  run govc vm.create -spec -c "$((max_cpu+100))" $vm
+  assert_success
+  spec="$output"
+
+  run govc vm.check.config -vm $vm -json <<<"$spec"
+  assert_success
+  assert_matches "vm requires 100 CPUs"
+
+  # spec.guestId
+  run govc vm.create -spec -g ttylinux -pool DC0_C0/Resources $vm
+  assert_success
+  spec="$output"
+
+  run govc vm.check.config -vm $vm -json <<<"$spec"
+  assert_success
+  assert_matches unsupported
+}
+
+@test "vm.check.compat" {
+  vcsim_env -cluster 2
+
+  export GOVC_SHOW_UNRELEASED=true
+
+  vm=DC0_C0_RP0_VM0
+
+  run govc vm.check.compat -vm $vm -pool DC0_C0/Resources
+  assert_success
+
+  run govc vm.check.compat -vm $vm -pool DC0_C1/Resources
+  assert_failure # InvalidArgument spec.pool
+}
+
+@test "vm.check.relocate" {
+  vcsim_env -cluster 2
+
+  export GOVC_SHOW_UNRELEASED=true
+
+  vm=DC0_C0_RP0_VM0
+  run govc vm.migrate -spec -pool DC0_C0/Resources $vm
+  assert_success
+  spec="$output"
+
+  run govc vm.check.relocate -vm $vm <<<"$spec"
+  assert_success
+}
diff --git a/simulator/vm_compatibility_checker_test.go b/simulator/vm_compatibility_checker_test.go
new file mode 100644
index 00000000..20f9c07a
--- /dev/null
+++ b/simulator/vm_compatibility_checker_test.go
@@ -0,0 +1,118 @@
+/*
+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 simulator_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+func TestVmCompatibilityChecker(t *testing.T) {
+	simulator.Test(func(ctx context.Context, c *vim25.Client) {
+		finder := find.NewFinder(c, true)
+		datacenter, err := finder.DefaultDatacenter(ctx)
+		if err != nil {
+			t.Fatalf("default datacenter not found: %s", err)
+		}
+		finder.SetDatacenter(datacenter)
+		vmList, err := finder.VirtualMachineList(ctx, "*")
+		if len(vmList) == 0 {
+			t.Fatal("vmList == 0")
+		}
+		vm := vmList[0]
+		vmRef := vm.Reference()
+
+		vmcc := object.NewVmCompatibilityChecker(c)
+
+		t.Run("CheckCompatibility", func(t *testing.T) {
+			results, err := vmcc.CheckCompatibility(
+				ctx,
+				vm.Reference(),
+				nil,
+				nil)
+
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			for _, result := range results {
+				if len(result.Error) > 0 {
+					t.Fatal("result has errors")
+				}
+				if len(result.Warning) > 0 {
+					t.Fatal("result has warnings")
+				}
+			}
+		})
+
+		t.Run("CheckVmConfig", func(t *testing.T) {
+			t.Run("for existing VM", func(t *testing.T) {
+				results, err := vmcc.CheckVmConfig(
+					ctx,
+					types.VirtualMachineConfigSpec{
+						NumCPUs: 2,
+					},
+					&vmRef,
+					nil,
+					nil)
+
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				for _, result := range results {
+					if len(result.Error) > 0 {
+						t.Fatal("result has errors")
+					}
+					if len(result.Warning) > 0 {
+						t.Fatal("result has warnings")
+					}
+				}
+			})
+			t.Run("for new VM", func(t *testing.T) {
+				results, err := vmcc.CheckVmConfig(
+					ctx,
+					types.VirtualMachineConfigSpec{
+						Name:    "my-vm",
+						NumCPUs: 2,
+					},
+					&vmRef,
+					nil,
+					nil)
+
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				for _, result := range results {
+					if len(result.Error) > 0 {
+						t.Fatal("result has errors")
+					}
+					if len(result.Warning) > 0 {
+						t.Fatal("result has warnings")
+					}
+				}
+			})
+		})
+	})
+}
diff --git a/simulator/vm_provisioning_checker_test.go b/simulator/vm_provisioning_checker_test.go
new file mode 100644
index 00000000..df442b51
--- /dev/null
+++ b/simulator/vm_provisioning_checker_test.go
@@ -0,0 +1,65 @@
+/*
+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 simulator_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+func TestVmProvisioningChecker(t *testing.T) {
+	simulator.Test(func(ctx context.Context, c *vim25.Client) {
+		finder := find.NewFinder(c, true)
+		datacenter, err := finder.DefaultDatacenter(ctx)
+		if err != nil {
+			t.Fatalf("default datacenter not found: %s", err)
+		}
+		finder.SetDatacenter(datacenter)
+		vmList, err := finder.VirtualMachineList(ctx, "*")
+		if len(vmList) == 0 {
+			t.Fatal("vmList == 0")
+		}
+		vm := vmList[0]
+
+		vmpc := object.NewVmProvisioningChecker(c)
+
+		t.Run("CheckRelocate", func(t *testing.T) {
+			results, err := vmpc.CheckRelocate(
+				ctx,
+				vm.Reference(),
+				types.VirtualMachineRelocateSpec{})
+
+			for _, result := range results {
+				if err != nil {
+					t.Fatal(err)
+				}
+				if len(result.Error) > 0 {
+					t.Fatal("result has errors")
+				}
+				if len(result.Warning) > 0 {
+					t.Fatal("result has warnings")
+				}
+			}
+		})
+	})
+}