Repository: vmware/govmomi
Pull request #3536 (2024-08-30 17:19:52)

vcsim: Support VM crypto spec in vC Sim


## Description

This patch adds support for "encrypting", "recrypting", and "decrypting" VMs in vC Sim. Please note, no encryption is actually used, and this change only impacts the state machine semantics. For example, if a VM is reconfigured to be encrypted, then retrieving the VM's "config.keyId" property will return the ID of the key and provider used to encrypt the VM.

Closes: `NA`

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

```
$ go test -v -count 1 -run 'TestEncryptDecryptVM' -coverprofile cover.out ./simulator
=== RUN   TestEncryptDecryptVM
=== RUN   TestEncryptDecryptVM/encrypt
=== RUN   TestEncryptDecryptVM/encrypt_w_already_encrypted
=== RUN   TestEncryptDecryptVM/encrypt_w_powered_on
=== RUN   TestEncryptDecryptVM/encrypt_w_snapshots
=== RUN   TestEncryptDecryptVM/decrypt
=== RUN   TestEncryptDecryptVM/decrypt_w_not_encrypted
=== RUN   TestEncryptDecryptVM/decrypt_w_powered_on
=== RUN   TestEncryptDecryptVM/decrypt_w_snapshots
=== RUN   TestEncryptDecryptVM/deep_recrypt
=== RUN   TestEncryptDecryptVM/deep_recrypt_w_same_provider_id
=== RUN   TestEncryptDecryptVM/deep_recrypt_w_not_encrypted
=== RUN   TestEncryptDecryptVM/deep_recrypt_w_powered_on
=== RUN   TestEncryptDecryptVM/deep_recrypt_w_snapshots
=== RUN   TestEncryptDecryptVM/shallow_recrypt
=== RUN   TestEncryptDecryptVM/shallow_recrypt_w_same_provider_id
=== RUN   TestEncryptDecryptVM/shallow_recrypt_w_not_encrypted
=== RUN   TestEncryptDecryptVM/shallow_recrypt_w_single_snapshot_chain
=== RUN   TestEncryptDecryptVM/shallow_recrypt_w_snapshot_tree
2024/08/30 12:14:08 *types.FileAlreadyExists: /var/folders/j4/r0gh7dx10fv4j0jwdfb6hz980000gq/T/govcsim-DC0-LocalDS_0-342415882/DC0_C0_RP0_VM0/DC0_C0_RP0_VM0-Snapshot1.vmsn
2024/08/30 12:14:08 *types.FileAlreadyExists: /var/folders/j4/r0gh7dx10fv4j0jwdfb6hz980000gq/T/govcsim-DC0-LocalDS_0-342415882/DC0_C0_RP0_VM0/DC0_C0_RP0_VM0-Snapshot1.vmsn
2024/08/30 12:14:08 *types.FileAlreadyExists: /var/folders/j4/r0gh7dx10fv4j0jwdfb6hz980000gq/T/govcsim-DC0-LocalDS_0-342415882/DC0_C0_RP0_VM0/DC0_C0_RP0_VM0-Snapshot2.vmsn
=== RUN   TestEncryptDecryptVM/noop
=== RUN   TestEncryptDecryptVM/register
--- PASS: TestEncryptDecryptVM (4.99s)
    --- PASS: TestEncryptDecryptVM/encrypt (0.24s)
    --- PASS: TestEncryptDecryptVM/encrypt_w_already_encrypted (0.25s)
    --- PASS: TestEncryptDecryptVM/encrypt_w_powered_on (0.27s)
    --- PASS: TestEncryptDecryptVM/encrypt_w_snapshots (0.27s)
    --- PASS: TestEncryptDecryptVM/decrypt (0.27s)
    --- PASS: TestEncryptDecryptVM/decrypt_w_not_encrypted (0.21s)
    --- PASS: TestEncryptDecryptVM/decrypt_w_powered_on (0.26s)
    --- PASS: TestEncryptDecryptVM/decrypt_w_snapshots (0.29s)
    --- PASS: TestEncryptDecryptVM/deep_recrypt (0.23s)
    --- PASS: TestEncryptDecryptVM/deep_recrypt_w_same_provider_id (0.24s)
    --- PASS: TestEncryptDecryptVM/deep_recrypt_w_not_encrypted (0.23s)
    --- PASS: TestEncryptDecryptVM/deep_recrypt_w_powered_on (0.23s)
    --- PASS: TestEncryptDecryptVM/deep_recrypt_w_snapshots (0.27s)
    --- PASS: TestEncryptDecryptVM/shallow_recrypt (0.23s)
    --- PASS: TestEncryptDecryptVM/shallow_recrypt_w_same_provider_id (0.24s)
    --- PASS: TestEncryptDecryptVM/shallow_recrypt_w_not_encrypted (0.24s)
    --- PASS: TestEncryptDecryptVM/shallow_recrypt_w_single_snapshot_chain (0.22s)
    --- PASS: TestEncryptDecryptVM/shallow_recrypt_w_snapshot_tree (0.32s)
    --- PASS: TestEncryptDecryptVM/noop (0.22s)
    --- PASS: TestEncryptDecryptVM/register (0.26s)
PASS
coverage: 27.6% of statements
ok  	github.com/vmware/govmomi/simulator	5.425s	coverage: 27.6% of statements
```

```
go tool cover -html=cover.out
```

The new function has 100% coverage:

<img width="905" alt="image" src="https://github.com/user-attachments/assets/67a353b5-5377-4d9a-8fa6-e87ffddbbb53">
<img width="905" alt="image" src="https://github.com/user-attachments/assets/62631ff5-2edb-4294-a634-06c16c1a22a8">


## Checklist:

- [x] My code follows the `CONTRIBUTION` [guidelines] of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged

[guidelines]: https://github.com/vmware/govmomi/blob/main/CONTRIBUTING.md



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..fcaa7ccb 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -1241,8 +1241,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 +1254,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
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/simulator/simulator.go b/simulator/simulator.go
index 31fd0eb7..4215df11 100644
--- a/simulator/simulator.go
+++ b/simulator/simulator.go
@@ -961,3 +961,19 @@ 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 263f41ac..4f13d47f 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -594,6 +594,12 @@ 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)
 }
 
@@ -1601,6 +1607,157 @@ 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"}
@@ -1921,22 +2078,6 @@ 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?


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/simulator/virtual_machine_test.go b/simulator/virtual_machine_test.go
index 4de23eec..3309e284 100644
--- a/simulator/virtual_machine_test.go
+++ b/simulator/virtual_machine_test.go
@@ -25,6 +25,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/assert"
+
 	"github.com/vmware/govmomi"
 	"github.com/vmware/govmomi/find"
 	"github.com/vmware/govmomi/object"
@@ -2651,3 +2653,593 @@ 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)
+		})
+	}
+}