From 35dc193f61c5d30f6127e25066a96d31209e6404 Mon Sep 17 00:00:00 2001 From: akutz Date: Fri, 30 Aug 2024 12:24:31 -0500 Subject: [PATCH] vcsim: Support VM crypto spec in vC Sim 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. --- simulator/simulator.go | 16 + simulator/virtual_machine.go | 173 ++++++++- simulator/virtual_machine_test.go | 592 ++++++++++++++++++++++++++++++ 3 files changed, 765 insertions(+), 16 deletions(-) diff --git a/simulator/simulator.go b/simulator/simulator.go index 31fd0eb78..4215df11b 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 263f41ac2..4f13d47f5 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? diff --git a/simulator/virtual_machine_test.go b/simulator/virtual_machine_test.go index 4de23eec9..3309e2842 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) + }) + } +}