Repository: vmware/govmomi
Pull request #3539 (2024-09-04 18:04:10)

Add support for using storage profiles when creating and attaching VirtualDisks
This includes support both First Class Disks and "classic" VM Disks.

govc: add disk.create '-profile' flag
govc: add vm.disk.create '-profile' flag
govc: add vm.disk.attach '-profile' flag
chore: govc: add common StorageProfileFlag
api: add VirtualMachine.AddDeviceWithProfile method
vcsim: add VirtualMachine AttachDisk and DetachDisk methods
govc add disk.attach and disk.detach commands
api: VirtualMachine.AttachDisk unitNumber param is optional


Patch:
diff --git a/cns/types/types.go b/cns/types/types.go
index c0aa669c..a649aedd 100644
--- a/cns/types/types.go
+++ b/cns/types/types.go
@@ -396,7 +396,6 @@ type CnsBlockBackingDetails struct {
 	BackingDiskUrlPath             string `xml:"backingDiskUrlPath,omitempty"`
 	BackingDiskObjectId            string `xml:"backingDiskObjectId,omitempty"`
 	AggregatedSnapshotCapacityInMb int64  `xml:"aggregatedSnapshotCapacityInMb,omitempty"`
-	BackingDiskPath                string `xml:"backingDiskPath,omitempty"`
 }
 
 func init() {
diff --git a/govc/USAGE.md b/govc/USAGE.md
index f5bde387..38549fe4 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -122,7 +122,9 @@ but appear via `govc $cmd -h`:
  - [device.serial.connect](#deviceserialconnect)
  - [device.serial.disconnect](#deviceserialdisconnect)
  - [device.usb.add](#deviceusbadd)
+ - [disk.attach](#diskattach)
  - [disk.create](#diskcreate)
+ - [disk.detach](#diskdetach)
  - [disk.ls](#diskls)
  - [disk.register](#diskregister)
  - [disk.rm](#diskrm)
@@ -1241,8 +1243,7 @@ Create datastore on HOST.
 
 Examples:
   govc datastore.create -type nfs -name nfsDatastore -remote-host 10.143.2.232 -remote-path /share cluster1
-  govc datastore.create -type vmfs -name vmfsDatastore -disk=mpx.vmhba0:C0:T0:L0 cluster1 # use entire disk
-  govc datastore.create -type vmfs -name vmfsDatastore -disk=mpx.vmhba0:C0:T0:L0 -size 20G cluster1 # use 20G of disk
+  govc datastore.create -type vmfs -name vmfsDatastore -disk=mpx.vmhba0:C0:T0:L0 cluster1
   govc datastore.create -type local -name localDatastore -path /var/datastore host1
 
 Options:
@@ -1255,7 +1256,6 @@ Options:
   -path=                 Local directory path for the datastore (local only)
   -remote-host=          Remote hostname of the NAS datastore
   -remote-path=          Remote path of the NFS mount point
-  -size=0B               Size of new disk. Default is to use entire disk
   -type=                 Datastore type (NFS|NFS41|CIFS|VMFS|local)
   -username=             Username to use when connecting (CIFS only)
   -version=<nil>         VMFS major version
@@ -2010,6 +2010,24 @@ Options:
   -vm=                   Virtual machine [GOVC_VM]
 ```
 
+## disk.attach
+
+```
+Usage: govc disk.attach [OPTIONS] ID
+
+Attach disk ID on VM.
+
+See also: govc vm.disk.attach
+
+Examples:
+  govc disk.attach -vm $vm ID
+  govc disk.attach -vm $vm -ds $ds ID
+
+Options:
+  -ds=                   Datastore [GOVC_DATASTORE]
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
 ## disk.create
 
 ```
@@ -2025,9 +2043,26 @@ Options:
   -ds=                   Datastore [GOVC_DATASTORE]
   -keep=<nil>            Keep disk after VM is deleted
   -pool=                 Resource pool [GOVC_RESOURCE_POOL]
+  -profile=[]            Storage profile name or ID
   -size=10.0GB           Size of new disk
 ```
 
+## disk.detach
+
+```
+Usage: govc disk.detach [OPTIONS] ID
+
+Detach disk ID from VM.
+
+See also: govc device.remove
+
+Examples:
+  govc disk.detach -vm $vm ID
+
+Options:
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
 ## disk.ls
 
 ```
@@ -3604,7 +3639,7 @@ Options:
   -m=false               Preserve MAC-addresses on network adapters
   -ovf=false             Clone as OVF (default is VM Template)
   -pool=                 Resource pool [GOVC_RESOURCE_POOL]
-  -profile=              Storage profile
+  -profile=[]            Storage profile name or ID
   -vm=                   Virtual machine [GOVC_VM]
 ```
 
@@ -3669,7 +3704,7 @@ Options:
   -host=                 Host system [GOVC_HOST]
   -options=              Options spec file path for VM deployment
   -pool=                 Resource pool [GOVC_RESOURCE_POOL]
-  -profile=              Storage profile
+  -profile=[]            Storage profile name or ID
 ```
 
 ## library.evict
@@ -4436,7 +4471,7 @@ Examples:
 Options:
   -cluster=              Cluster [GOVC_CLUSTER]
   -library=[]            Content library IDs to associate with the vSphere Namespace.
-  -storage=[]            Storage profile IDs to associate with the vSphere Namespace.
+  -storage=[]            Storage profile name or ID
   -vmclass=[]            Virtual machine class IDs to associate with the vSphere Namespace.
 ```
 
@@ -4611,7 +4646,7 @@ Examples:
 
 Options:
   -library=[]            Content library IDs to associate with the vSphere Namespace.
-  -storage=[]            Storage profile IDs to associate with the vSphere Namespace.
+  -storage=[]            Storage profile name or ID
   -vmclass=[]            Virtual machine class IDs to associate with the vSphere Namespace.
 ```
 
@@ -6451,7 +6486,7 @@ Options:
   -net.protocol=         Network device protocol. Applicable to vmxnet3vrdma. Default to 'rocev2'
   -on=true               Power on VM
   -pool=                 Resource pool [GOVC_RESOURCE_POOL]
-  -profile=              Storage profile name or ID
+  -profile=[]            Storage profile name or ID
   -version=              ESXi hardware version [2|3|4|5.0|5.1|5.5|6.0|6.5|6.7|6.7.2|7.0|7.0.1|7.0.2|8.0|8.0.1|8.0.2]
 ```
 
@@ -6695,6 +6730,7 @@ Options:
   -link=true             Link specified disk
   -mode=                 Disk mode (persistent|nonpersistent|undoable|independent_persistent|independent_nonpersistent|append)
   -persist=true          Persist attached disk
+  -profile=[]            Storage profile name or ID
   -sharing=              Sharing (sharingNone|sharingMultiWriter)
   -vm=                   Virtual machine [GOVC_VM]
 ```
@@ -6743,6 +6779,7 @@ Options:
   -eager=false           Eagerly scrub new disk
   -mode=persistent       Disk mode (persistent|nonpersistent|undoable|independent_persistent|independent_nonpersistent|append)
   -name=                 Name for new disk
+  -profile=[]            Storage profile name or ID
   -sharing=              Sharing (sharingNone|sharingMultiWriter)
   -size=10.0GB           Size of new disk
   -thick=false           Thick provision new disk
diff --git a/govc/datastore/create.go b/govc/datastore/create.go
index e3ca7547..a3f78d65 100644
--- a/govc/datastore/create.go
+++ b/govc/datastore/create.go
@@ -28,16 +28,10 @@ import (
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
 	"github.com/vmware/govmomi/object"
-	"github.com/vmware/govmomi/units"
 	"github.com/vmware/govmomi/vim25/soap"
 	"github.com/vmware/govmomi/vim25/types"
 )
 
-const (
-	sectorSize  = 512  // Sector size in bytes
-	startSector = 2048 // Start sector (typically fixed)
-)
-
 type create struct {
 	*flags.HostSystemFlag
 
@@ -56,7 +50,6 @@ type create struct {
 	// Options for VMFS
 	DiskCanonicalName string
 	Version           *int32
-	Size              units.ByteSize
 
 	// Options for local
 	Path string
@@ -151,7 +144,6 @@ func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
 	// Options for VMFS
 	f.StringVar(&cmd.DiskCanonicalName, "disk", "", "Canonical name of disk (VMFS only)")
 	f.Var(flags.NewOptionalInt32(&cmd.Version), "version", "VMFS major version")
-	f.Var(&cmd.Size, "size", "Size of new disk. Default is to use entire disk")
 
 	// Options for Local
 	f.StringVar(&cmd.Path, "path", "", "Local directory path for the datastore (local only)")
@@ -173,8 +165,7 @@ func (cmd *create) Description() string {
 
 Examples:
   govc datastore.create -type nfs -name nfsDatastore -remote-host 10.143.2.232 -remote-path /share cluster1
-  govc datastore.create -type vmfs -name vmfsDatastore -disk=mpx.vmhba0:C0:T0:L0 cluster1 # use entire disk
-  govc datastore.create -type vmfs -name vmfsDatastore -disk=mpx.vmhba0:C0:T0:L0 -size 20G cluster1 # use 20G of disk
+  govc datastore.create -type vmfs -name vmfsDatastore -disk=mpx.vmhba0:C0:T0:L0 cluster1
   govc datastore.create -type local -name localDatastore -path /var/datastore host1`
 }
 
@@ -298,12 +289,6 @@ func (cmd *create) CreateVmfsDatastore(ctx context.Context, hosts []*object.Host
 
 		spec := *option.Spec.(*types.VmfsDatastoreCreateSpec)
 		spec.Vmfs.VolumeName = cmd.Name
-		if cmd.Size > 0 {
-			endSector := CalculateSectors(int64(cmd.Size))
-			// set values for Sectors
-			spec.Partition.Partition[0].StartSector = startSector
-			spec.Partition.Partition[0].EndSector = endSector
-		}
 		if cmd.Version != nil {
 			spec.Vmfs.MajorVersion = *cmd.Version
 		}
@@ -316,14 +301,6 @@ func (cmd *create) CreateVmfsDatastore(ctx context.Context, hosts []*object.Host
 	return nil
 }
 
-// CalculateSectors calculates the start and end sectors based on the given size.
-func CalculateSectors(sizeInBytes int64) (endSector int64) {
-	totalSectors := sizeInBytes / sectorSize
-	endSector = startSector + totalSectors - 1
-
-	return endSector
-}
-
 func (cmd *create) CreateLocalDatastore(ctx context.Context, hosts []*object.HostSystem) error {
 	for _, host := range hosts {
 		ds, err := host.ConfigManager().DatastoreSystem(ctx)
diff --git a/govc/disk/attach.go b/govc/disk/attach.go
new file mode 100644
index 00000000..3f7f959c
--- /dev/null
+++ b/govc/disk/attach.go
@@ -0,0 +1,97 @@
+/*
+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 disk
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vim25/mo"
+)
+
+type attach struct {
+	*flags.VirtualMachineFlag
+	*flags.DatastoreFlag
+}
+
+func init() {
+	cli.Register("disk.attach", &attach{})
+}
+
+func (cmd *attach) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx)
+	cmd.DatastoreFlag.Register(ctx, f)
+}
+
+func (cmd *attach) Process(ctx context.Context) error {
+	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
+		return err
+	}
+	return cmd.DatastoreFlag.Process(ctx)
+}
+
+func (cmd *attach) Usage() string {
+	return "ID"
+}
+
+func (cmd *attach) Description() string {
+	return `Attach disk ID on VM.
+
+See also: govc vm.disk.attach
+
+Examples:
+  govc disk.attach -vm $vm ID
+  govc disk.attach -vm $vm -ds $ds ID`
+}
+
+func (cmd *attach) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	ds, err := cmd.DatastoreIfSpecified()
+	if err != nil {
+		return err
+	}
+
+	vm, err := cmd.VirtualMachine()
+	if err != nil {
+		return err
+	}
+
+	if ds == nil {
+		var props mo.VirtualMachine
+		err = vm.Properties(ctx, vm.Reference(), []string{"datastore"}, &props)
+		if err != nil {
+			return err
+		}
+		if len(props.Datastore) != 1 {
+			ds, err = cmd.Datastore() // likely results in MultipleFoundError
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	id := f.Arg(0)
+
+	return vm.AttachDisk(ctx, id, ds, 0, nil)
+}
diff --git a/govc/disk/create.go b/govc/disk/create.go
index e7f8958a..49db2343 100644
--- a/govc/disk/create.go
+++ b/govc/disk/create.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2018 VMware, Inc. All Rights Reserved.
+Copyright (c) 2018-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,
@@ -34,6 +34,7 @@ type disk struct {
 	*flags.DatastoreFlag
 	*flags.ResourcePoolFlag
 	*flags.StoragePodFlag
+	*flags.StorageProfileFlag
 
 	size units.ByteSize
 	keep *bool
@@ -50,6 +51,9 @@ func (cmd *disk) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.StoragePodFlag, ctx = flags.NewStoragePodFlag(ctx)
 	cmd.StoragePodFlag.Register(ctx, f)
 
+	cmd.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx)
+	cmd.StorageProfileFlag.Register(ctx, f)
+
 	cmd.ResourcePoolFlag, ctx = flags.NewResourcePoolFlag(ctx)
 	cmd.ResourcePoolFlag.Register(ctx, f)
 
@@ -65,6 +69,9 @@ func (cmd *disk) Process(ctx context.Context) error {
 	if err := cmd.StoragePodFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.StorageProfileFlag.Process(ctx); err != nil {
+		return err
+	}
 	return cmd.ResourcePoolFlag.Process(ctx)
 }
 
@@ -108,12 +115,18 @@ func (cmd *disk) Run(ctx context.Context, f *flag.FlagSet) error {
 		}
 	}
 
+	profile, err := cmd.StorageProfileSpec(ctx)
+	if err != nil {
+		return err
+	}
+
 	m := vslm.NewObjectManager(c)
 
 	spec := types.VslmCreateSpec{
 		Name:              name,
 		CapacityInMB:      int64(cmd.size) / units.MB,
 		KeepAfterDeleteVm: cmd.keep,
+		Profile:           profile,
 		BackingSpec: &types.VslmCreateSpecDiskFileBackingSpec{
 			VslmCreateSpecBackingSpec: types.VslmCreateSpecBackingSpec{
 				Datastore: ds.Reference(),
diff --git a/govc/disk/detach.go b/govc/disk/detach.go
new file mode 100644
index 00000000..b135528b
--- /dev/null
+++ b/govc/disk/detach.go
@@ -0,0 +1,66 @@
+/*
+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 disk
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+)
+
+type detach struct {
+	*flags.VirtualMachineFlag
+}
+
+func init() {
+	cli.Register("disk.detach", &detach{})
+}
+
+func (cmd *detach) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+}
+
+func (cmd *detach) Usage() string {
+	return "ID"
+}
+
+func (cmd *detach) Description() string {
+	return `Detach disk ID from VM.
+
+See also: govc device.remove
+
+Examples:
+  govc disk.detach -vm $vm ID`
+}
+
+func (cmd *detach) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	vm, err := cmd.VirtualMachine()
+	if err != nil {
+		return err
+	}
+
+	id := f.Arg(0)
+
+	return vm.DetachDisk(ctx, id)
+}
diff --git a/govc/flags/storage_profile.go b/govc/flags/storage_profile.go
new file mode 100644
index 00000000..3dc1dab3
--- /dev/null
+++ b/govc/flags/storage_profile.go
@@ -0,0 +1,124 @@
+/*
+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 flags
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type StorageProfileFlag struct {
+	*ClientFlag
+
+	Name []string
+
+	option string
+}
+
+func NewStorageProfileFlag(ctx context.Context, option ...string) (*StorageProfileFlag, context.Context) {
+	v := &StorageProfileFlag{}
+	if len(option) == 1 {
+		v.option = option[0]
+	} else {
+		v.option = "profile"
+	}
+	v.ClientFlag, ctx = NewClientFlag(ctx)
+	return v, ctx
+}
+
+func (e *StorageProfileFlag) String() string {
+	return fmt.Sprint(e.Name)
+}
+
+func (e *StorageProfileFlag) Set(value string) error {
+	e.Name = append(e.Name, value)
+	return nil
+}
+
+func (flag *StorageProfileFlag) Register(ctx context.Context, f *flag.FlagSet) {
+	flag.ClientFlag.Register(ctx, f)
+
+	f.Var(flag, flag.option, "Storage profile name or ID")
+}
+
+func (flag *StorageProfileFlag) StorageProfileList(ctx context.Context) ([]string, error) {
+	if len(flag.Name) == 0 {
+		return nil, nil
+	}
+
+	c, err := flag.PbmClient()
+	if err != nil {
+		return nil, err
+	}
+	m, err := c.ProfileMap(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	list := make([]string, len(flag.Name))
+
+	for i, name := range flag.Name {
+		p, ok := m.Name[name]
+		if !ok {
+			return nil, fmt.Errorf("storage profile %q not found", name)
+		}
+
+		list[i] = p.GetPbmProfile().ProfileId.UniqueId
+	}
+
+	return list, nil
+}
+
+func (flag *StorageProfileFlag) StorageProfile(ctx context.Context) (string, error) {
+	switch len(flag.Name) {
+	case 0:
+		return "", nil
+	case 1:
+	default:
+		return "", errors.New("only 1 '-profile' can be specified")
+	}
+
+	list, err := flag.StorageProfileList(ctx)
+	if err != nil {
+		return "", err
+	}
+
+	return list[0], nil
+}
+
+func (flag *StorageProfileFlag) StorageProfileSpec(ctx context.Context) ([]types.BaseVirtualMachineProfileSpec, error) {
+	if len(flag.Name) == 0 {
+		return nil, nil
+	}
+
+	list, err := flag.StorageProfileList(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	spec := make([]types.BaseVirtualMachineProfileSpec, len(list))
+	for i, name := range list {
+		spec[i] = &types.VirtualMachineDefinedProfileSpec{
+			ProfileId: name,
+		}
+	}
+	return spec, nil
+}
diff --git a/govc/library/clone.go b/govc/library/clone.go
index c72d78fd..06d75194 100644
--- a/govc/library/clone.go
+++ b/govc/library/clone.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2019 VMware, Inc. All Rights Reserved.
+Copyright (c) 2019-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,
@@ -33,11 +33,11 @@ type clone struct {
 	*flags.ResourcePoolFlag
 	*flags.HostSystemFlag
 	*flags.VirtualMachineFlag
+	*flags.StorageProfileFlag
 
-	profile string
-	ovf     bool
-	extra   bool
-	mac     bool
+	ovf   bool
+	extra bool
+	mac   bool
 }
 
 func init() {
@@ -63,10 +63,12 @@ func (cmd *clone) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
 	cmd.VirtualMachineFlag.Register(ctx, f)
 
+	cmd.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx)
+	cmd.StorageProfileFlag.Register(ctx, f)
+
 	f.BoolVar(&cmd.ovf, "ovf", false, "Clone as OVF (default is VM Template)")
 	f.BoolVar(&cmd.extra, "e", false, "Include extra configuration")
 	f.BoolVar(&cmd.mac, "m", false, "Preserve MAC-addresses on network adapters")
-	f.StringVar(&cmd.profile, "profile", "", "Storage profile")
 }
 
 func (cmd *clone) Process(ctx context.Context) error {
@@ -85,6 +87,9 @@ func (cmd *clone) Process(ctx context.Context) error {
 	if err := cmd.FolderFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.StorageProfileFlag.Process(ctx); err != nil {
+		return err
+	}
 	return cmd.VirtualMachineFlag.Process(ctx)
 }
 
@@ -176,14 +181,19 @@ func (cmd *clone) Run(ctx context.Context, f *flag.FlagSet) error {
 		return nil
 	}
 
+	profile, err := cmd.StorageProfile(ctx)
+	if err != nil {
+		return err
+	}
+
 	storage := &vcenter.DiskStorage{
 		Datastore: dsID,
 		StoragePolicy: &vcenter.StoragePolicy{
-			Policy: cmd.profile,
+			Policy: profile,
 			Type:   "USE_SOURCE_POLICY",
 		},
 	}
-	if cmd.profile != "" {
+	if profile != "" {
 		storage.StoragePolicy.Type = "USE_SPECIFIED_POLICY"
 	}
 
diff --git a/govc/library/deploy.go b/govc/library/deploy.go
index e3a17890..57cfee6f 100644
--- a/govc/library/deploy.go
+++ b/govc/library/deploy.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2019 VMware, Inc. All Rights Reserved.
+Copyright (c) 2019-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,
@@ -35,10 +35,10 @@ type deploy struct {
 	*flags.ResourcePoolFlag
 	*flags.HostSystemFlag
 	*flags.FolderFlag
+	*flags.StorageProfileFlag
 	*importx.OptionsFlag
 
-	profile string
-	config  string
+	config string
 }
 
 func init() {
@@ -58,11 +58,12 @@ func (cmd *deploy) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.FolderFlag, ctx = flags.NewFolderFlag(ctx)
 	cmd.FolderFlag.Register(ctx, f)
 
+	cmd.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx)
+	cmd.StorageProfileFlag.Register(ctx, f)
+
 	cmd.OptionsFlag = new(importx.OptionsFlag)
 	cmd.OptionsFlag.Register(ctx, f)
 
-	f.StringVar(&cmd.profile, "profile", "", "Storage profile")
-
 	if cli.ShowUnreleased() {
 		f.StringVar(&cmd.config, "config", "", "VM config spec")
 	}
@@ -81,6 +82,9 @@ func (cmd *deploy) Process(ctx context.Context) error {
 	if err := cmd.FolderFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.StorageProfileFlag.Process(ctx); err != nil {
+		return err
+	}
 	return cmd.OptionsFlag.Process(ctx)
 }
 
@@ -114,13 +118,13 @@ func (cmd *deploy) Run(ctx context.Context, f *flag.FlagSet) error {
 	if err != nil {
 		return err
 	}
-	cmd.KeepAlive(vc)
+	cmd.FolderFlag.KeepAlive(vc)
 
 	c, err := cmd.DatastoreFlag.RestClient()
 	if err != nil {
 		return err
 	}
-	cmd.KeepAlive(c)
+	cmd.FolderFlag.KeepAlive(c)
 
 	m := vcenter.NewManager(c)
 
@@ -194,6 +198,11 @@ func (cmd *deploy) Run(ctx context.Context, f *flag.FlagSet) error {
 
 	var ref *types.ManagedObjectReference
 
+	profile, err := cmd.StorageProfile(ctx)
+	if err != nil {
+		return err
+	}
+
 	switch item.Type {
 	case library.ItemTypeOVF:
 		deploy := vcenter.Deploy{
@@ -217,7 +226,7 @@ func (cmd *deploy) Run(ctx context.Context, f *flag.FlagSet) error {
 				},
 				NetworkMappings:     networks,
 				StorageProvisioning: cmd.Options.DiskProvisioning,
-				StorageProfileID:    cmd.profile,
+				StorageProfileID:    profile,
 			},
 			Target: vcenter.Target{
 				ResourcePoolID: rp.Reference().Value,
@@ -241,11 +250,11 @@ func (cmd *deploy) Run(ctx context.Context, f *flag.FlagSet) error {
 		storage := &vcenter.DiskStorage{
 			Datastore: dsID,
 			StoragePolicy: &vcenter.StoragePolicy{
-				Policy: cmd.profile,
+				Policy: profile,
 				Type:   "USE_SOURCE_POLICY",
 			},
 		}
-		if cmd.profile != "" {
+		if profile != "" {
 			storage.StoragePolicy.Type = "USE_SPECIFIED_POLICY"
 		}
 
diff --git a/govc/namespace/create.go b/govc/namespace/create.go
index e4e99737..dc37e052 100644
--- a/govc/namespace/create.go
+++ b/govc/namespace/create.go
@@ -88,7 +88,7 @@ func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
 		return flag.ErrHelp
 	}
 
-	rc, err := cmd.RestClient()
+	rc, err := cmd.namespaceFlag.RestClient()
 	if err != nil {
 		return err
 	}
diff --git a/govc/namespace/flag.go b/govc/namespace/flag.go
index a0db25b3..3f50fb07 100644
--- a/govc/namespace/flag.go
+++ b/govc/namespace/flag.go
@@ -25,24 +25,23 @@ import (
 )
 
 type namespaceFlag struct {
-	*flags.ClientFlag
+	*flags.StorageProfileFlag
 
 	library flags.StringList
 	vmclass flags.StringList
-	storage flags.StringList
+	storage []string
 }
 
 func (ns *namespaceFlag) Register(ctx context.Context, f *flag.FlagSet) {
-	ns.ClientFlag, ctx = flags.NewClientFlag(ctx)
-	ns.ClientFlag.Register(ctx, f)
+	ns.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx, "storage")
+	ns.StorageProfileFlag.Register(ctx, f)
 
 	f.Var(&ns.library, "library", "Content library IDs to associate with the vSphere Namespace.")
 	f.Var(&ns.vmclass, "vmclass", "Virtual machine class IDs to associate with the vSphere Namespace.")
-	f.Var(&ns.storage, "storage", "Storage profile IDs to associate with the vSphere Namespace.")
 }
 
 func (ns *namespaceFlag) Process(ctx context.Context) error {
-	if err := ns.ClientFlag.Process(ctx); err != nil {
+	if err := ns.StorageProfileFlag.Process(ctx); err != nil {
 		return err
 	}
 
@@ -58,23 +57,9 @@ func (ns *namespaceFlag) Process(ctx context.Context) error {
 		}
 	}
 
-	pc, err := ns.PbmClient()
-	if err != nil {
-		return err
-	}
-
-	m, err := pc.ProfileMap(ctx)
-	if err != nil {
-		return err
-	}
-
-	for i, name := range ns.storage {
-		if n, ok := m.Name[name]; ok {
-			ns.storage[i] = n.GetPbmProfile().ProfileId.UniqueId
-		}
-	}
+	ns.storage, err = ns.StorageProfileList(ctx)
 
-	return nil
+	return err
 }
 
 func (ns *namespaceFlag) storageSpec() []namespace.StorageSpec {
diff --git a/govc/vm/create.go b/govc/vm/create.go
index 757fc2ea..35f98d97 100644
--- a/govc/vm/create.go
+++ b/govc/vm/create.go
@@ -51,6 +51,7 @@ type create struct {
 	*flags.HostSystemFlag
 	*flags.NetworkFlag
 	*flags.FolderFlag
+	*flags.StorageProfileFlag
 
 	name       string
 	memory     int
@@ -64,7 +65,6 @@ type create struct {
 	firmware   string
 	version    string
 	place      bool
-	profile    string
 
 	iso              string
 	isoDatastoreFlag *flags.DatastoreFlag
@@ -119,6 +119,9 @@ func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.FolderFlag, ctx = flags.NewFolderFlag(ctx)
 	cmd.FolderFlag.Register(ctx, f)
 
+	cmd.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx)
+	cmd.StorageProfileFlag.Register(ctx, f)
+
 	f.IntVar(&cmd.memory, "m", 1024, "Size in MB of memory")
 	f.IntVar(&cmd.cpus, "c", 1, "Number of CPUs")
 	f.StringVar(&cmd.guestID, "g", "otherGuest", "Guest OS ID")
@@ -128,7 +131,6 @@ func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
 	f.StringVar(&cmd.controller, "disk.controller", "scsi", "Disk controller type")
 	f.StringVar(&cmd.annotation, "annotation", "", "VM description")
 	f.StringVar(&cmd.firmware, "firmware", FirmwareTypes[0], FirmwareUsage)
-	f.StringVar(&cmd.profile, "profile", "", "Storage profile name or ID")
 	if cli.ShowUnreleased() {
 		f.BoolVar(&cmd.place, "place", false, "Place VM without creating")
 	}
@@ -178,6 +180,9 @@ func (cmd *create) Process(ctx context.Context) error {
 	if err := cmd.FolderFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.StorageProfileFlag.Process(ctx); err != nil {
+		return err
+	}
 
 	// Default iso/disk datastores to the VM's datastore
 	if cmd.isoDatastoreFlag.Name == "" {
@@ -411,22 +416,9 @@ func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 		Version:    cmd.version,
 	}
 
-	if cmd.profile != "" {
-		c, err := cmd.PbmClient()
-		if err != nil {
-			return nil, err
-		}
-		m, err := c.ProfileMap(ctx)
-		if err != nil {
-			return nil, err
-		}
-		p, ok := m.Name[cmd.profile]
-		if !ok {
-			return nil, fmt.Errorf("profile %q not found", cmd.profile)
-		}
-		spec.VmProfile = []types.BaseVirtualMachineProfileSpec{&types.VirtualMachineDefinedProfileSpec{
-			ProfileId: p.GetPbmProfile().ProfileId.UniqueId,
-		}}
+	spec.VmProfile, err = cmd.StorageProfileSpec(ctx)
+	if err != nil {
+		return nil, err
 	}
 
 	devices, err = cmd.addStorage(nil)
diff --git a/govc/vm/disk/attach.go b/govc/vm/disk/attach.go
index 6e06aecd..36cf2d40 100644
--- a/govc/vm/disk/attach.go
+++ b/govc/vm/disk/attach.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
+Copyright (c) 2014-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,
@@ -30,6 +30,7 @@ import (
 type attach struct {
 	*flags.DatastoreFlag
 	*flags.VirtualMachineFlag
+	*flags.StorageProfileFlag
 
 	persist    bool
 	link       bool
@@ -48,6 +49,8 @@ func (cmd *attach) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.DatastoreFlag.Register(ctx, f)
 	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
 	cmd.VirtualMachineFlag.Register(ctx, f)
+	cmd.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx)
+	cmd.StorageProfileFlag.Register(ctx, f)
 
 	f.BoolVar(&cmd.persist, "persist", true, "Persist attached disk")
 	f.BoolVar(&cmd.link, "link", true, "Link specified disk")
@@ -64,6 +67,9 @@ func (cmd *attach) Process(ctx context.Context) error {
 	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.StorageProfileFlag.Process(ctx); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -129,5 +135,10 @@ func (cmd *attach) Run(ctx context.Context, f *flag.FlagSet) error {
 		backing.DiskMode = cmd.mode
 	}
 
-	return vm.AddDevice(ctx, disk)
+	profile, err := cmd.StorageProfileSpec(ctx)
+	if err != nil {
+		return err
+	}
+
+	return vm.AddDeviceWithProfile(ctx, profile, disk)
 }
diff --git a/govc/vm/disk/create.go b/govc/vm/disk/create.go
index 44822ff4..7c4b27fb 100644
--- a/govc/vm/disk/create.go
+++ b/govc/vm/disk/create.go
@@ -33,6 +33,7 @@ type create struct {
 	*flags.DatastoreFlag
 	*flags.OutputFlag
 	*flags.VirtualMachineFlag
+	*flags.StorageProfileFlag
 
 	controller string
 	Name       string
@@ -58,6 +59,8 @@ func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
 	cmd.OutputFlag.Register(ctx, f)
 	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
 	cmd.VirtualMachineFlag.Register(ctx, f)
+	cmd.StorageProfileFlag, ctx = flags.NewStorageProfileFlag(ctx)
+	cmd.StorageProfileFlag.Register(ctx, f)
 
 	err := (&cmd.Bytes).Set("10G")
 	if err != nil {
@@ -83,6 +86,9 @@ func (cmd *create) Process(ctx context.Context) error {
 	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
 		return err
 	}
+	if err := cmd.StorageProfileFlag.Process(ctx); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -112,6 +118,11 @@ func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
 		return err
 	}
 
+	profile, err := cmd.StorageProfileSpec(ctx)
+	if err != nil {
+		return err
+	}
+
 	devices, err := vm.Device(ctx)
 	if err != nil {
 		return err
@@ -154,5 +165,5 @@ func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
 
 	_, _ = cmd.Log("Creating disk\n")
 	disk.CapacityInKB = int64(cmd.Bytes) / 1024
-	return vm.AddDevice(ctx, disk)
+	return vm.AddDeviceWithProfile(ctx, profile, disk)
 }
diff --git a/object/virtual_machine.go b/object/virtual_machine.go
index 4665fcb7..3f8f6ebe 100644
--- a/object/virtual_machine.go
+++ b/object/virtual_machine.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2015-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2015-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.
@@ -500,7 +500,7 @@ func diskFileOperation(op types.VirtualDeviceConfigSpecOperation, fop types.Virt
 	return ""
 }
 
-func (v VirtualMachine) configureDevice(ctx context.Context, op types.VirtualDeviceConfigSpecOperation, fop types.VirtualDeviceConfigSpecFileOperation, devices ...types.BaseVirtualDevice) error {
+func (v VirtualMachine) configureDevice(ctx context.Context, profile []types.BaseVirtualMachineProfileSpec, op types.VirtualDeviceConfigSpecOperation, fop types.VirtualDeviceConfigSpecFileOperation, devices ...types.BaseVirtualDevice) error {
 	spec := types.VirtualMachineConfigSpec{}
 
 	for _, device := range devices {
@@ -508,6 +508,7 @@ func (v VirtualMachine) configureDevice(ctx context.Context, op types.VirtualDev
 			Device:        device,
 			Operation:     op,
 			FileOperation: diskFileOperation(op, fop, device),
+			Profile:       profile,
 		}
 
 		spec.DeviceChange = append(spec.DeviceChange, config)
@@ -523,12 +524,22 @@ func (v VirtualMachine) configureDevice(ctx context.Context, op types.VirtualDev
 
 // AddDevice adds the given devices to the VirtualMachine
 func (v VirtualMachine) AddDevice(ctx context.Context, device ...types.BaseVirtualDevice) error {
-	return v.configureDevice(ctx, types.VirtualDeviceConfigSpecOperationAdd, types.VirtualDeviceConfigSpecFileOperationCreate, device...)
+	return v.AddDeviceWithProfile(ctx, nil, device...)
+}
+
+// AddDeviceWithProfile adds the given devices to the VirtualMachine with the given profile
+func (v VirtualMachine) AddDeviceWithProfile(ctx context.Context, profile []types.BaseVirtualMachineProfileSpec, device ...types.BaseVirtualDevice) error {
+	return v.configureDevice(ctx, profile, types.VirtualDeviceConfigSpecOperationAdd, types.VirtualDeviceConfigSpecFileOperationCreate, device...)
 }
 
 // EditDevice edits the given (existing) devices on the VirtualMachine
 func (v VirtualMachine) EditDevice(ctx context.Context, device ...types.BaseVirtualDevice) error {
-	return v.configureDevice(ctx, types.VirtualDeviceConfigSpecOperationEdit, types.VirtualDeviceConfigSpecFileOperationReplace, device...)
+	return v.EditDeviceWithProfile(ctx, nil, device...)
+}
+
+// EditDeviceWithProfile edits the given (existing) devices on the VirtualMachine with the given profile
+func (v VirtualMachine) EditDeviceWithProfile(ctx context.Context, profile []types.BaseVirtualMachineProfileSpec, device ...types.BaseVirtualDevice) error {
+	return v.configureDevice(ctx, profile, types.VirtualDeviceConfigSpecOperationEdit, types.VirtualDeviceConfigSpecFileOperationReplace, device...)
 }
 
 // RemoveDevice removes the given devices on the VirtualMachine
@@ -537,17 +548,17 @@ func (v VirtualMachine) RemoveDevice(ctx context.Context, keepFiles bool, device
 	if keepFiles {
 		fop = ""
 	}
-	return v.configureDevice(ctx, types.VirtualDeviceConfigSpecOperationRemove, fop, device...)
+	return v.configureDevice(ctx, nil, types.VirtualDeviceConfigSpecOperationRemove, fop, device...)
 }
 
 // AttachDisk attaches the given disk to the VirtualMachine
-func (v VirtualMachine) AttachDisk(ctx context.Context, id string, datastore *Datastore, controllerKey int32, unitNumber int32) error {
+func (v VirtualMachine) AttachDisk(ctx context.Context, id string, datastore *Datastore, controllerKey int32, unitNumber *int32) error {
 	req := types.AttachDisk_Task{
 		This:          v.Reference(),
 		DiskId:        types.ID{Id: id},
 		Datastore:     datastore.Reference(),
 		ControllerKey: controllerKey,
-		UnitNumber:    &unitNumber,
+		UnitNumber:    unitNumber,
 	}
 
 	res, err := methods.AttachDisk_Task(ctx, v.c, &req)
diff --git a/simulator/simulator.go b/simulator/simulator.go
index 4215df11..31fd0eb7 100644
--- a/simulator/simulator.go
+++ b/simulator/simulator.go
@@ -961,19 +961,3 @@ func UnmarshalBody(typeFunc func(string) (reflect.Type, bool), data []byte) (*Me
 
 	return method, nil
 }
-
-func newInvalidStateFault(format string, args ...any) *types.InvalidState {
-	msg := fmt.Sprintf(format, args...)
-	return &types.InvalidState{
-		VimFault: types.VimFault{
-			MethodFault: types.MethodFault{
-				FaultCause: &types.LocalizedMethodFault{
-					Fault: &types.SystemErrorFault{
-						Reason: msg,
-					},
-					LocalizedMessage: msg,
-				},
-			},
-		},
-	}
-}
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 4f13d47f..8448cd77 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -594,12 +594,6 @@ func (vm *VirtualMachine) configure(ctx *Context, spec *types.VirtualMachineConf
 		}
 	}
 
-	if spec.Crypto != nil {
-		if err := vm.updateCrypto(ctx, spec.Crypto); err != nil {
-			return err
-		}
-	}
-
 	return vm.configureDevices(ctx, spec)
 }
 
@@ -1607,157 +1601,6 @@ func (vm *VirtualMachine) genVmdkPath(p object.DatastorePath) (string, types.Bas
 	}
 }
 
-// Encrypt requires powered off VM with no snapshots.
-// Decrypt requires powered off VM.
-// Deep recrypt requires powered off VM with no snapshots.
-// Shallow recrypt works with VMs in any power state and even if snapshots are
-// present as long as it is a single chain and not a tree.
-func (vm *VirtualMachine) updateCrypto(
-	ctx *Context,
-	spec types.BaseCryptoSpec) types.BaseMethodFault {
-
-	const configKeyId = "config.keyId"
-
-	assertEncrypted := func() types.BaseMethodFault {
-		if vm.Config.KeyId == nil {
-			return newInvalidStateFault("vm is not encrypted")
-		}
-		return nil
-	}
-
-	assertPoweredOff := func() types.BaseMethodFault {
-		if vm.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOff {
-			return &types.InvalidPowerState{
-				ExistingState:  vm.Runtime.PowerState,
-				RequestedState: types.VirtualMachinePowerStatePoweredOff,
-			}
-		}
-		return nil
-	}
-
-	assertNoSnapshots := func(allowSingleChain bool) types.BaseMethodFault {
-		hasSnapshots := vm.Snapshot != nil && vm.Snapshot.CurrentSnapshot != nil
-		if !hasSnapshots {
-			return nil
-		}
-		if !allowSingleChain {
-			return newInvalidStateFault("vm has snapshots")
-		}
-		type node = types.VirtualMachineSnapshotTree
-		var isTreeFn func(nodes []node) types.BaseMethodFault
-		isTreeFn = func(nodes []node) types.BaseMethodFault {
-			switch len(nodes) {
-			case 0:
-				return nil
-			case 1:
-				return isTreeFn(nodes[0].ChildSnapshotList)
-			default:
-				return newInvalidStateFault("vm has snapshot tree")
-			}
-		}
-		return isTreeFn(vm.Snapshot.RootSnapshotList)
-	}
-
-	doRecrypt := func(newKeyID types.CryptoKeyId) types.BaseMethodFault {
-		if err := assertEncrypted(); err != nil {
-			return err
-		}
-		var providerID *types.KeyProviderId
-		if pid := vm.Config.KeyId.ProviderId; pid != nil {
-			providerID = &types.KeyProviderId{
-				Id: pid.Id,
-			}
-		}
-		if pid := newKeyID.ProviderId; pid != nil {
-			providerID = &types.KeyProviderId{
-				Id: pid.Id,
-			}
-		}
-		ctx.Map.Update(vm, []types.PropertyChange{
-			{
-				Name: configKeyId,
-				Op:   types.PropertyChangeOpAssign,
-				Val: &types.CryptoKeyId{
-					KeyId:      newKeyID.KeyId,
-					ProviderId: providerID,
-				},
-			},
-		})
-		return nil
-	}
-
-	switch tspec := spec.(type) {
-	case *types.CryptoSpecDecrypt:
-		if err := assertPoweredOff(); err != nil {
-			return err
-		}
-		if err := assertNoSnapshots(false); err != nil {
-			return err
-		}
-		if err := assertEncrypted(); err != nil {
-			return err
-		}
-		ctx.Map.Update(vm, []types.PropertyChange{
-			{
-				Name: configKeyId,
-				Op:   types.PropertyChangeOpRemove,
-				Val:  nil,
-			},
-		})
-
-	case *types.CryptoSpecDeepRecrypt:
-		if err := assertPoweredOff(); err != nil {
-			return err
-		}
-		if err := assertNoSnapshots(false); err != nil {
-			return err
-		}
-		return doRecrypt(tspec.NewKeyId)
-
-	case *types.CryptoSpecShallowRecrypt:
-		if err := assertNoSnapshots(true); err != nil {
-			return err
-		}
-		return doRecrypt(tspec.NewKeyId)
-
-	case *types.CryptoSpecEncrypt:
-		if err := assertPoweredOff(); err != nil {
-			return err
-		}
-		if err := assertNoSnapshots(false); err != nil {
-			return err
-		}
-		if vm.Config.KeyId != nil {
-			return newInvalidStateFault("vm is already encrypted")
-		}
-
-		var providerID *types.KeyProviderId
-		if pid := tspec.CryptoKeyId.ProviderId; pid != nil {
-			providerID = &types.KeyProviderId{
-				Id: pid.Id,
-			}
-		}
-
-		ctx.Map.Update(vm, []types.PropertyChange{
-			{
-				Name: configKeyId,
-				Op:   types.PropertyChangeOpAssign,
-				Val: &types.CryptoKeyId{
-					KeyId:      tspec.CryptoKeyId.KeyId,
-					ProviderId: providerID,
-				},
-			},
-		})
-
-	case *types.CryptoSpecNoOp,
-		*types.CryptoSpecRegister:
-
-		// No-op
-	}
-
-	return nil
-}
-
 func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMachineConfigSpec) types.BaseMethodFault {
 	var changes []types.PropertyChange
 	field := mo.Field{Path: "config.hardware.device"}
@@ -2078,6 +1921,22 @@ func (vm *VirtualMachine) UpgradeVMTask(ctx *Context, req *types.UpgradeVM_Task)
 
 	task := CreateTask(vm, "upgradeVm", func(t *Task) (types.AnyType, types.BaseMethodFault) {
 
+		newInvalidStateFault := func(format string, args ...any) *types.InvalidState {
+			msg := fmt.Sprintf(format, args...)
+			return &types.InvalidState{
+				VimFault: types.VimFault{
+					MethodFault: types.MethodFault{
+						FaultCause: &types.LocalizedMethodFault{
+							Fault: &types.SystemErrorFault{
+								Reason: msg,
+							},
+							LocalizedMessage: msg,
+						},
+					},
+				},
+			}
+		}
+
 		// InvalidPowerState
 		//
 		// 1. Is VM's power state anything other than powered off?
@@ -2746,6 +2605,63 @@ func (vm *VirtualMachine) RemoveAllSnapshotsTask(ctx *Context, req *types.Remove
 	}
 }
 
+func (vm *VirtualMachine) fcd(ctx *Context, ds types.ManagedObjectReference, id types.ID) *VStorageObject {
+	m := ctx.Map.Get(*ctx.Map.content().VStorageObjectManager).(*VcenterVStorageObjectManager)
+	if ds.Value != "" {
+		return m.objects[ds][id]
+	}
+	for _, set := range m.objects {
+		for key, val := range set {
+			if key == id {
+				return val
+			}
+		}
+	}
+	return nil
+}
+
+func (vm *VirtualMachine) AttachDiskTask(ctx *Context, req *types.AttachDisk_Task) soap.HasFault {
+	task := CreateTask(vm, "attachDisk", func(t *Task) (types.AnyType, types.BaseMethodFault) {
+		fcd := vm.fcd(ctx, req.Datastore, req.DiskId)
+		if fcd == nil {
+			return nil, new(types.InvalidArgument)
+		}
+
+		fcd.Config.ConsumerId = []types.ID{{Id: vm.Config.Uuid}}
+
+		// TODO: add device
+
+		return nil, nil
+	})
+
+	return &methods.AttachDisk_TaskBody{
+		Res: &types.AttachDisk_TaskResponse{
+			Returnval: task.Run(ctx),
+		},
+	}
+}
+
+func (vm *VirtualMachine) DetachDiskTask(ctx *Context, req *types.DetachDisk_Task) soap.HasFault {
+	task := CreateTask(vm, "detachDisk", func(t *Task) (types.AnyType, types.BaseMethodFault) {
+		fcd := vm.fcd(ctx, types.ManagedObjectReference{}, req.DiskId)
+		if fcd == nil {
+			return nil, new(types.InvalidArgument)
+		}
+
+		fcd.Config.ConsumerId = nil
+
+		// TODO: remove device
+
+		return nil, nil
+	})
+
+	return &methods.DetachDisk_TaskBody{
+		Res: &types.DetachDisk_TaskResponse{
+			Returnval: task.Run(ctx),
+		},
+	}
+}
+
 func (vm *VirtualMachine) ShutdownGuest(ctx *Context, c *types.ShutdownGuest) soap.HasFault {
 	r := &methods.ShutdownGuestBody{}
 
diff --git a/simulator/vstorage_object_manager.go b/simulator/vstorage_object_manager.go
index 5dc138e0..451dc043 100644
--- a/simulator/vstorage_object_manager.go
+++ b/simulator/vstorage_object_manager.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2018 VMware, Inc. All Rights Reserved.
+Copyright (c) 2018-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,
@@ -287,6 +287,10 @@ func (m *VcenterVStorageObjectManager) DeleteVStorageObjectTask(ctx *Context, re
 			return nil, &types.InvalidArgument{}
 		}
 
+		if len(obj.Config.ConsumerId) != 0 {
+			return nil, &types.InvalidState{}
+		}
+
 		backing := obj.Config.Backing.(*types.BaseConfigInfoDiskFileBackingInfo)
 		ds := ctx.Map.Get(req.Datastore).(*Datastore)
 		dc := ctx.Map.getEntityDatacenter(ds)


Test patch:
diff --git a/cns/client_test.go b/cns/client_test.go
index 412bf07d..8fe07e35 100644
--- a/cns/client_test.go
+++ b/cns/client_test.go
@@ -308,27 +308,6 @@ func TestClient(t *testing.T) {
 		}
 	}
 
-	// Test BackingDiskPath field
-	var queryFilterBackingDiskPathTest cnstypes.CnsQueryFilter
-	var volumeIDListBackingDiskPathTest []cnstypes.CnsVolumeId
-	volumeIDListBackingDiskPathTest = append(volumeIDListBackingDiskPathTest, cnstypes.CnsVolumeId{Id: volumeId})
-	queryFilterBackingDiskPathTest.VolumeIds = volumeIDListBackingDiskPathTest
-	t.Logf("Calling QueryVolume using queryFilter: %+v", pretty.Sprint(queryFilterBackingDiskPathTest))
-	queryResultBackingDiskPathTest, err := cnsClient.QueryVolume(ctx, queryFilterBackingDiskPathTest)
-	if err != nil {
-		t.Errorf("Failed to query all volumes. Error: %+v \n", err)
-		t.Fatal(err)
-	}
-	t.Logf("Successfully Queried Volumes. queryResultBackingDiskPathTest: %+v", pretty.Sprint(queryResultBackingDiskPathTest))
-	t.Log("Checking backingDiskPath retrieved")
-	for _, vol := range queryResultBackingDiskPathTest.Volumes {
-		backingDiskPath := vol.BackingObjectDetails.(*cnstypes.CnsBlockBackingDetails).BackingDiskPath
-		if backingDiskPath == "" {
-			t.Errorf("Failed to get BackingDiskPath")
-			t.FailNow()
-		}
-	}
-
 	// Test QuerySnapshots API on 7.0 U3 or above
 	var snapshotQueryFilter cnstypes.CnsSnapshotQueryFilter
 	var querySnapshotsTaskResult *cnstypes.CnsSnapshotQueryResult
diff --git a/govc/test/disk.bats b/govc/test/disk.bats
index cf151a42..53048786 100755
--- a/govc/test/disk.bats
+++ b/govc/test/disk.bats
@@ -26,11 +26,26 @@ load test_helper
 
   govc disk.ls -json "$id" | jq .
 
+  vm=DC0_H0_VM0
+
+  run govc disk.attach -vm $vm "$id"
+  assert_success
+
+  run govc disk.detach -vm $vm "$id"
+  assert_success
+
   run govc disk.rm "$id"
   assert_success
 
   run govc disk.rm "$id"
   assert_failure
+
+  name=$(new_id)
+  run govc disk.create -profile enoent "$name"
+  assert_failure # profile does not exist
+
+  run govc disk.create -profile "vSAN Default Storage Policy" "$name"
+  assert_success
 }
 
 @test "disk.create -datastore-cluster" {
diff --git a/govc/test/namespace.bats b/govc/test/namespace.bats
index 0ca1fc90..09a4c419 100755
--- a/govc/test/namespace.bats
+++ b/govc/test/namespace.bats
@@ -130,6 +130,15 @@ load test_helper
 
     ns=$(govc namespace.info -json test-namespace-2 | jq)
     assert_equal "2" $(echo $ns | jq -r '."vm_service_spec"."content_libraries"' | jq length)
+
+    run govc namespace.create -cluster DC0_C0 -storage MyStoragePolicy test-namespace-2
+    assert_failure # storage policy does not exist
+
+    run govc storage.policy.create -category my_cat -tag my_tag MyStoragePolicy
+    assert_success
+
+    run govc namespace.create -cluster DC0_C0 -storage MyStoragePolicy test-namespace-2
+    assert_success
 }
 
 @test "namespace.update" {
diff --git a/govc/test/vm.bats b/govc/test/vm.bats
index da01c59b..7628a247 100755
--- a/govc/test/vm.bats
+++ b/govc/test/vm.bats
@@ -695,6 +695,13 @@ load test_helper
 
   run govc vm.disk.change -vm "$vm" -disk.name "$disk" -size 1M
   assert_failure # cannot shrink disk
+
+  name=$(new_id)
+  run govc vm.disk.create -vm "$vm" -name "$vm/$name" -profile enoent
+  assert_failure # profile does not exist
+
+  run govc vm.disk.create -vm "$vm" -name "$vm/$name" -profile "vSAN Default Storage Policy"
+  assert_success
 }
 
 @test "vm.disk.attach" {
diff --git a/simulator/virtual_machine_test.go b/simulator/virtual_machine_test.go
index 3309e284..4de23eec 100644
--- a/simulator/virtual_machine_test.go
+++ b/simulator/virtual_machine_test.go
@@ -25,8 +25,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
-
 	"github.com/vmware/govmomi"
 	"github.com/vmware/govmomi/find"
 	"github.com/vmware/govmomi/object"
@@ -2653,593 +2651,3 @@ func TestUpgradeVm(t *testing.T) {
 
 	}, model)
 }
-
-func TestEncryptDecryptVM(t *testing.T) {
-
-	newTaskErrWithInvalidState := func(msg string) error {
-		return task.Error{
-			LocalizedMethodFault: &types.LocalizedMethodFault{
-				Fault:            newInvalidStateFault(msg),
-				LocalizedMessage: "*types.InvalidState",
-			},
-		}
-	}
-
-	newTaskErrWithInvalidPowerState := func(cur, req types.VirtualMachinePowerState) error {
-		return task.Error{
-			LocalizedMethodFault: &types.LocalizedMethodFault{
-				Fault: &types.InvalidPowerState{
-					ExistingState:  cur,
-					RequestedState: req,
-				},
-				LocalizedMessage: "*types.InvalidPowerState",
-			},
-		}
-	}
-
-	testCases := []struct {
-		name                string
-		initStateFn         func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error
-		configSpec          types.VirtualMachineConfigSpec
-		expectedCryptoKeyId *types.CryptoKeyId
-		expectedErr         error
-	}{
-		{
-			name: "encrypt",
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecEncrypt{
-					CryptoKeyId: types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "123",
-				ProviderId: &types.KeyProviderId{
-					Id: "abc",
-				},
-			},
-		},
-		{
-			name: "encrypt w already encrypted",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecEncrypt{
-					CryptoKeyId: types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm is already encrypted"),
-		},
-		{
-			name: "encrypt w powered on",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				tsk, err := object.NewVirtualMachine(c, vmRef).PowerOn(ctx)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecEncrypt{
-					CryptoKeyId: types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidPowerState(
-				types.VirtualMachinePowerStatePoweredOn,
-				types.VirtualMachinePowerStatePoweredOff,
-			),
-		},
-		{
-			name: "encrypt w snapshots",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				tsk, err := object.NewVirtualMachine(c, vmRef).CreateSnapshot(
-					ctx, "root", "", false, false)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecEncrypt{
-					CryptoKeyId: types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm has snapshots"),
-		},
-		{
-			name: "decrypt",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDecrypt{},
-			},
-			expectedCryptoKeyId: nil,
-		},
-		{
-			name: "decrypt w not encrypted",
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDecrypt{},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm is not encrypted"),
-		},
-		{
-			name: "decrypt w powered on",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				tsk, err := object.NewVirtualMachine(c, vmRef).PowerOn(ctx)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDecrypt{},
-			},
-			expectedErr: newTaskErrWithInvalidPowerState(
-				types.VirtualMachinePowerStatePoweredOn,
-				types.VirtualMachinePowerStatePoweredOff,
-			),
-		},
-		{
-			name: "decrypt w snapshots",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				tsk, err := object.NewVirtualMachine(c, vmRef).CreateSnapshot(
-					ctx, "root", "", false, false)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDecrypt{},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm has snapshots"),
-		},
-		{
-			name: "deep recrypt",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDeepRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "456",
-				ProviderId: &types.KeyProviderId{
-					Id: "def",
-				},
-			},
-		},
-		{
-			name: "deep recrypt w same provider id",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDeepRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "456",
-				ProviderId: &types.KeyProviderId{
-					Id: "abc",
-				},
-			},
-		},
-		{
-			name: "deep recrypt w not encrypted",
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDeepRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm is not encrypted"),
-		},
-		{
-			name: "deep recrypt w powered on",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				tsk, err := object.NewVirtualMachine(c, vmRef).PowerOn(ctx)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDeepRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidPowerState(
-				types.VirtualMachinePowerStatePoweredOn,
-				types.VirtualMachinePowerStatePoweredOff,
-			),
-		},
-		{
-			name: "deep recrypt w snapshots",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				tsk, err := object.NewVirtualMachine(c, vmRef).CreateSnapshot(
-					ctx, "root", "", false, false)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecDeepRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm has snapshots"),
-		},
-		{
-			name: "shallow recrypt",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecShallowRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "456",
-				ProviderId: &types.KeyProviderId{
-					Id: "def",
-				},
-			},
-		},
-		{
-			name: "shallow recrypt w same provider id",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecShallowRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "456",
-				ProviderId: &types.KeyProviderId{
-					Id: "abc",
-				},
-			},
-		},
-		{
-			name: "shallow recrypt w not encrypted",
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecShallowRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm is not encrypted"),
-		},
-		{
-			name: "shallow recrypt w single snapshot chain",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				tsk, err := object.NewVirtualMachine(c, vmRef).CreateSnapshot(
-					ctx, "root", "", false, false)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecShallowRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "456",
-				ProviderId: &types.KeyProviderId{
-					Id: "def",
-				},
-			},
-		},
-		{
-			name: "shallow recrypt w snapshot tree",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				vm := object.NewVirtualMachine(c, vmRef)
-				tsk, err := vm.CreateSnapshot(ctx, "root", "", false, false)
-				if err != nil {
-					return err
-				}
-				if err := tsk.Wait(ctx); err != nil {
-					return err
-				}
-				for i := 0; i < 2; i++ {
-					tsk, err := vm.CreateSnapshot(
-						ctx,
-						fmt.Sprintf("snap-%d", i),
-						"",
-						false,
-						false)
-					if err != nil {
-						return err
-					}
-					if err := tsk.Wait(ctx); err != nil {
-						return err
-					}
-					tsk, err = vm.RevertToSnapshot(ctx, "root", true)
-					if err != nil {
-						return err
-					}
-					if err := tsk.Wait(ctx); err != nil {
-						return err
-					}
-				}
-				tsk, err = object.NewVirtualMachine(c, vmRef).PowerOn(ctx)
-				if err != nil {
-					return err
-				}
-				return tsk.Wait(ctx)
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecShallowRecrypt{
-					NewKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedErr: newTaskErrWithInvalidState("vm has snapshot tree"),
-		},
-		{
-			name: "noop",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecNoOp{},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "123",
-				ProviderId: &types.KeyProviderId{
-					Id: "abc",
-				},
-			},
-		},
-		{
-			name: "register",
-			initStateFn: func(ctx *Context, c *vim25.Client, vmRef types.ManagedObjectReference) error {
-				Map.WithLock(ctx, vmRef, func() {
-					Map.Get(vmRef).(*VirtualMachine).Config.KeyId = &types.CryptoKeyId{
-						KeyId: "123",
-						ProviderId: &types.KeyProviderId{
-							Id: "abc",
-						},
-					}
-				})
-				return nil
-			},
-			configSpec: types.VirtualMachineConfigSpec{
-				Crypto: &types.CryptoSpecRegister{
-					CryptoKeyId: types.CryptoKeyId{
-						KeyId: "456",
-						ProviderId: &types.KeyProviderId{
-							Id: "def",
-						},
-					},
-				},
-			},
-			expectedCryptoKeyId: &types.CryptoKeyId{
-				KeyId: "123",
-				ProviderId: &types.KeyProviderId{
-					Id: "abc",
-				},
-			},
-		},
-	}
-
-	for i := range testCases {
-		tc := testCases[i]
-		t.Run(tc.name, func(t *testing.T) {
-
-			model := VPX()
-			model.Autostart = false
-			model.Cluster = 1
-			model.ClusterHost = 1
-			model.Host = 1
-
-			Test(func(ctx context.Context, c *vim25.Client) {
-
-				ref := Map.Any("VirtualMachine").Reference()
-				vm := object.NewVirtualMachine(c, ref)
-
-				if tc.initStateFn != nil {
-					if err := tc.initStateFn(SpoofContext(), c, ref); err != nil {
-						t.Fatalf("initStateFn failed: %v", err)
-					}
-				}
-
-				tsk, err := vm.Reconfigure(context.TODO(), tc.configSpec)
-				assert.NoError(t, err)
-
-				if a, e := tsk.Wait(context.TODO()), tc.expectedErr; e != nil {
-					assert.Equal(t, e, a)
-				} else {
-					if !assert.NoError(t, a) {
-						return
-					}
-					var moVM mo.VirtualMachine
-					if err := vm.Properties(ctx, ref, []string{"config.keyId"}, &moVM); err != nil {
-						t.Fatalf("fetching properties failed: %v", err)
-					}
-					if tc.expectedCryptoKeyId != nil {
-						assert.Equal(t, tc.expectedCryptoKeyId, moVM.Config.KeyId)
-					} else {
-						assert.Nil(t, moVM.Config)
-					}
-				}
-			}, model)
-		})
-	}
-}