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) + }) + } +}