From b5dda1ba7b9f5d80401226ce0e4f372374661f5b Mon Sep 17 00:00:00 2001 From: Sai Diliyaer Date: Wed, 18 Oct 2023 17:42:48 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=B1=20Add=20VM=20backup=20implementati?= =?UTF-8?q?on=20for=20v1a2=20(#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds the implementation for VM backup in v1a2. It includes all the follow-up changes in PR #239 with some minor updates specific to v1a2. --- .../v1alpha2/virtualmachine_controller.go | 9 - .../virtualmachine_controller_unit_test.go | 23 -- pkg/vmprovider/fake/fake_vm_provider_a2.go | 12 - pkg/vmprovider/interface_a2.go | 1 - .../providers/vsphere2/constants/constants.go | 13 + .../vsphere2/virtualmachine/backup.go | 237 ++++++++++++++++++ .../vsphere2/virtualmachine/backup_test.go | 229 +++++++++++++++++ .../virtualmachine_suite_test.go | 1 + .../providers/vsphere2/vmprovider_vm.go | 19 +- 9 files changed, 493 insertions(+), 51 deletions(-) create mode 100644 pkg/vmprovider/providers/vsphere2/virtualmachine/backup.go create mode 100644 pkg/vmprovider/providers/vsphere2/virtualmachine/backup_test.go diff --git a/controllers/virtualmachine/v1alpha2/virtualmachine_controller.go b/controllers/virtualmachine/v1alpha2/virtualmachine_controller.go index 838d3a0ac..ceaea80f9 100644 --- a/controllers/virtualmachine/v1alpha2/virtualmachine_controller.go +++ b/controllers/virtualmachine/v1alpha2/virtualmachine_controller.go @@ -336,15 +336,6 @@ func (r *Reconciler) ReconcileNormal(ctx *context.VirtualMachineContextA2) (rete // Add this VM to prober manager if ReconcileNormal succeeds. r.Prober.AddToProberManager(ctx.VM) - // Back up this VM if ReconcileNormal succeeds and the FSS is enabled. - if lib.IsVMServiceBackupRestoreFSSEnabled() { - if err := r.VMProvider.BackupVirtualMachine(ctx, ctx.VM); err != nil { - ctx.Logger.Error(err, "Failed to backup VirtualMachine") - r.Recorder.EmitEvent(ctx.VM, "Backup", err, false) - return err - } - } - ctx.Logger.Info("Finished Reconciling VirtualMachine") return nil } diff --git a/controllers/virtualmachine/v1alpha2/virtualmachine_controller_unit_test.go b/controllers/virtualmachine/v1alpha2/virtualmachine_controller_unit_test.go index f9f8a6f07..80d7d1a68 100644 --- a/controllers/virtualmachine/v1alpha2/virtualmachine_controller_unit_test.go +++ b/controllers/virtualmachine/v1alpha2/virtualmachine_controller_unit_test.go @@ -6,7 +6,6 @@ package v1alpha2_test import ( "context" "errors" - "os" "strings" . "github.com/onsi/ginkgo" @@ -19,7 +18,6 @@ import ( virtualmachine "github.com/vmware-tanzu/vm-operator/controllers/virtualmachine/v1alpha2" vmopContext "github.com/vmware-tanzu/vm-operator/pkg/context" - "github.com/vmware-tanzu/vm-operator/pkg/lib" proberfake "github.com/vmware-tanzu/vm-operator/pkg/prober2/fake" providerfake "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/fake" "github.com/vmware-tanzu/vm-operator/test/builder" @@ -155,27 +153,6 @@ func unitTestsReconcile() { Expect(reconciler.ReconcileNormal(vmCtx)).Should(Succeed()) Expect(fakeProbeManager.IsAddToProberManagerCalled).Should(BeTrue()) }) - - When("The VM Service Backup and Restore FSS is enabled", func() { - BeforeEach(func() { - Expect(os.Setenv(lib.VMServiceBackupRestoreFSS, lib.TrueString)).To(Succeed()) - }) - - AfterEach(func() { - Expect(os.Unsetenv(lib.VMServiceBackupRestoreFSS)).To(Succeed()) - }) - - It("Should call backup Virtual Machine if ReconcileNormal succeeds", func() { - var isBackupVirtualMachineCalled bool - fakeVMProvider.BackupVirtualMachineFn = func(ctx context.Context, vm *vmopv1.VirtualMachine) error { - isBackupVirtualMachineCalled = true - return nil - } - - Expect(reconciler.ReconcileNormal(vmCtx)).Should(Succeed()) - Expect(isBackupVirtualMachineCalled).Should(BeTrue()) - }) - }) }) Context("ReconcileDelete", func() { diff --git a/pkg/vmprovider/fake/fake_vm_provider_a2.go b/pkg/vmprovider/fake/fake_vm_provider_a2.go index ccbea29fe..b0e8dcf1f 100644 --- a/pkg/vmprovider/fake/fake_vm_provider_a2.go +++ b/pkg/vmprovider/fake/fake_vm_provider_a2.go @@ -31,7 +31,6 @@ type funcsA2 struct { DeleteVirtualMachineFn func(ctx context.Context, vm *vmopv1.VirtualMachine) error PublishVirtualMachineFn func(ctx context.Context, vm *vmopv1.VirtualMachine, vmPub *vmopv1.VirtualMachinePublishRequest, cl *imgregv1a1.ContentLibrary, actID string) (string, error) - BackupVirtualMachineFn func(ctx context.Context, vm *vmopv1.VirtualMachine) error GetVirtualMachineGuestHeartbeatFn func(ctx context.Context, vm *vmopv1.VirtualMachine) (vmopv1.GuestHeartbeatStatus, error) GetVirtualMachineWebMKSTicketFn func(ctx context.Context, vm *vmopv1.VirtualMachine, pubKey string) (string, error) GetVirtualMachineHardwareVersionFn func(ctx context.Context, vm *vmopv1.VirtualMachine) (int32, error) @@ -113,17 +112,6 @@ func (s *VMProviderA2) PublishVirtualMachine(ctx context.Context, vm *vmopv1.Vir return "dummy-id", nil } -func (s *VMProviderA2) BackupVirtualMachine(ctx context.Context, vm *vmopv1.VirtualMachine) error { - s.Lock() - defer s.Unlock() - - if s.BackupVirtualMachineFn != nil { - return s.BackupVirtualMachineFn(ctx, vm) - } - - return nil -} - func (s *VMProviderA2) GetVirtualMachineGuestHeartbeat(ctx context.Context, vm *vmopv1.VirtualMachine) (vmopv1.GuestHeartbeatStatus, error) { s.Lock() defer s.Unlock() diff --git a/pkg/vmprovider/interface_a2.go b/pkg/vmprovider/interface_a2.go index 784d47afc..2b9819c26 100644 --- a/pkg/vmprovider/interface_a2.go +++ b/pkg/vmprovider/interface_a2.go @@ -21,7 +21,6 @@ type VirtualMachineProviderInterfaceA2 interface { DeleteVirtualMachine(ctx context.Context, vm *v1alpha2.VirtualMachine) error PublishVirtualMachine(ctx context.Context, vm *v1alpha2.VirtualMachine, vmPub *v1alpha2.VirtualMachinePublishRequest, cl *imgregv1a1.ContentLibrary, actID string) (string, error) - BackupVirtualMachine(ctx context.Context, vm *v1alpha2.VirtualMachine) error GetVirtualMachineGuestHeartbeat(ctx context.Context, vm *v1alpha2.VirtualMachine) (v1alpha2.GuestHeartbeatStatus, error) GetVirtualMachineWebMKSTicket(ctx context.Context, vm *v1alpha2.VirtualMachine, pubKey string) (string, error) GetVirtualMachineHardwareVersion(ctx context.Context, vm *v1alpha2.VirtualMachine) (int32, error) diff --git a/pkg/vmprovider/providers/vsphere2/constants/constants.go b/pkg/vmprovider/providers/vsphere2/constants/constants.go index ef26052b7..725b1ad0c 100644 --- a/pkg/vmprovider/providers/vsphere2/constants/constants.go +++ b/pkg/vmprovider/providers/vsphere2/constants/constants.go @@ -134,4 +134,17 @@ const ( V1alpha2SubnetMask = "V1alpha2_SubnetMask" // V1alpha2FormatNameservers is an alias for versioned templating function V1alpha2_FormatNameservers. V1alpha2FormatNameservers = "V1alpha2_FormatNameservers" + + // BackupVMKubeDataExtraConfigKey is the ExtraConfig key to the VirtualMachine + // resource's Kubernetes spec data, compressed using gzip and base64-encoded. + BackupVMKubeDataExtraConfigKey = "vmservice.virtualmachine.kubedata" + // BackupVMBootstrapDataExtraConfigKey is the ExtraConfig key to the VM's + // bootstrap data object, compressed using gzip and base64-encoded. + BackupVMBootstrapDataExtraConfigKey = "vmservice.virtualmachine.bootstrapdata" + // BackupVMDiskDataExtraConfigKey is the ExtraConfig key to the VM's disk info + // data in JSON, compressed using gzip and base64-encoded. + BackupVMDiskDataExtraConfigKey = "vmservice.virtualmachine.diskdata" + // BackupVMCloudInitInstanceIDExtraConfigKey is the ExtraConfig key to the VM's + // Cloud-Init instance ID, compressed using gzip and base64-encoded. + BackupVMCloudInitInstanceIDExtraConfigKey = "vmservice.virtualmachine.cloudinit.instanceid" ) diff --git a/pkg/vmprovider/providers/vsphere2/virtualmachine/backup.go b/pkg/vmprovider/providers/vsphere2/virtualmachine/backup.go new file mode 100644 index 000000000..bf40f2181 --- /dev/null +++ b/pkg/vmprovider/providers/vsphere2/virtualmachine/backup.go @@ -0,0 +1,237 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package virtualmachine + +import ( + goctx "context" + "encoding/json" + + "sigs.k8s.io/yaml" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" + "github.com/vmware-tanzu/vm-operator/pkg/context" + "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/constants" +) + +type VMDiskData struct { + // ID of the virtual disk object (only set for FCDs). + VDiskID string + // Filename contains the datastore path to the virtual disk. + FileName string +} + +// BackupVirtualMachine backs up the required data of a VM into its ExtraConfig. +// Currently, the following data is backed up: +// - Kubernetes VirtualMachine object in YAML format (without its .status field). +// - VM bootstrap data in JSON (if provided). +// - List of VM disk data in JSON (including FCDs attached to the VM). +func BackupVirtualMachine( + vmCtx context.VirtualMachineContextA2, + vcVM *object.VirtualMachine, + bootstrapData map[string]string) error { + var moVM mo.VirtualMachine + if err := vcVM.Properties(vmCtx, vcVM.Reference(), + []string{"config.extraConfig"}, &moVM); err != nil { + vmCtx.Logger.Error(err, "Failed to get VM properties for backup") + return err + } + curEcMap := util.ExtraConfigToMap(moVM.Config.ExtraConfig) + + var ecToUpdate []types.BaseOptionValue + + vmKubeDataBackup, err := getDesiredVMKubeDataForBackup(vmCtx.VM, curEcMap) + if err != nil { + vmCtx.Logger.Error(err, "Failed to get VM kube data for backup") + return err + } + if vmKubeDataBackup == "" { + vmCtx.Logger.V(4).Info("Skipping VM kube data backup as unchanged") + } else { + ecToUpdate = append(ecToUpdate, &types.OptionValue{ + Key: constants.BackupVMKubeDataExtraConfigKey, + Value: vmKubeDataBackup, + }) + } + + instanceIDBackup, err := getDesiredCloudInitInstanceIDForBackup(vmCtx.VM, curEcMap) + if err != nil { + vmCtx.Logger.Error(err, "Failed to get cloud-init instance ID for backup") + return err + } + if instanceIDBackup == "" { + vmCtx.Logger.V(4).Info("Skipping cloud-init instance ID as already stored") + } else { + ecToUpdate = append(ecToUpdate, &types.OptionValue{ + Key: constants.BackupVMCloudInitInstanceIDExtraConfigKey, + Value: instanceIDBackup, + }) + } + + bootstrapDataBackup, err := getDesiredBootstrapDataForBackup(bootstrapData, curEcMap) + if err != nil { + vmCtx.Logger.Error(err, "Failed to get VM bootstrap data for backup") + return err + } + if bootstrapDataBackup == "" { + vmCtx.Logger.V(4).Info("Skipping VM bootstrap data backup as unchanged") + } else { + ecToUpdate = append(ecToUpdate, &types.OptionValue{ + Key: constants.BackupVMBootstrapDataExtraConfigKey, + Value: bootstrapDataBackup, + }) + } + + diskDataBackup, err := getDesiredDiskDataForBackup(vmCtx, vcVM, curEcMap) + if err != nil { + vmCtx.Logger.Error(err, "Failed to get VM disk data for backup") + return err + } + if diskDataBackup == "" { + vmCtx.Logger.V(4).Info("Skipping VM disk data backup as unchanged") + } else { + ecToUpdate = append(ecToUpdate, &types.OptionValue{ + Key: constants.BackupVMDiskDataExtraConfigKey, + Value: diskDataBackup, + }) + } + + if len(ecToUpdate) != 0 { + vmCtx.Logger.Info("Updating VM ExtraConfig with backup data") + vmCtx.Logger.V(4).Info("", "ExtraConfig", ecToUpdate) + if _, err := vcVM.Reconfigure(vmCtx, types.VirtualMachineConfigSpec{ + ExtraConfig: ecToUpdate, + }); err != nil { + vmCtx.Logger.Error(err, "Failed to update VM ExtraConfig for backup") + return err + } + } + + return nil +} + +func getDesiredVMKubeDataForBackup( + vm *vmopv1.VirtualMachine, + ecMap map[string]string) (string, error) { + // If the ExtraConfig already contains the latest VM spec, determined by + // 'metadata.generation', return an empty string to skip the backup. + if ecKubeData, ok := ecMap[constants.BackupVMKubeDataExtraConfigKey]; ok { + vmFromBackup, err := constructVMObj(ecKubeData) + if err != nil { + return "", err + } + if vmFromBackup.ObjectMeta.Generation >= vm.ObjectMeta.Generation { + return "", nil + } + } + + backupVM := vm.DeepCopy() + backupVM.Status = vmopv1.VirtualMachineStatus{} + backupVMYaml, err := yaml.Marshal(backupVM) + if err != nil { + return "", err + } + + return util.EncodeGzipBase64(string(backupVMYaml)) +} + +func constructVMObj(ecKubeData string) (vmopv1.VirtualMachine, error) { + var vmObj vmopv1.VirtualMachine + decodedKubeData, err := util.TryToDecodeBase64Gzip([]byte(ecKubeData)) + if err != nil { + return vmObj, err + } + + err = yaml.Unmarshal([]byte(decodedKubeData), &vmObj) + return vmObj, err +} + +func getDesiredCloudInitInstanceIDForBackup( + vm *vmopv1.VirtualMachine, + ecMap map[string]string) (string, error) { + // Cloud-Init instance ID should not be changed once persisted in VM's + // ExtraConfig. Return an empty string to skip the backup if it exists. + if _, ok := ecMap[constants.BackupVMCloudInitInstanceIDExtraConfigKey]; ok { + return "", nil + } + + instanceID := vm.Annotations[vmopv1.InstanceIDAnnotation] + if instanceID == "" { + instanceID = string(vm.UID) + } + + return util.EncodeGzipBase64(instanceID) +} + +func getDesiredBootstrapDataForBackup( + bootstrapDataRaw map[string]string, + ecMap map[string]string) (string, error) { + // No bootstrap data is specified, return an empty string to skip the backup. + if len(bootstrapDataRaw) == 0 { + return "", nil + } + + bootstrapDataJSON, err := json.Marshal(bootstrapDataRaw) + if err != nil { + return "", err + } + bootstrapDataBackup, err := util.EncodeGzipBase64(string(bootstrapDataJSON)) + if err != nil { + return "", err + } + + // Return an empty string to skip the backup if the data is unchanged. + if bootstrapDataBackup == ecMap[constants.BackupVMBootstrapDataExtraConfigKey] { + return "", nil + } + + return bootstrapDataBackup, nil +} + +func getDesiredDiskDataForBackup( + ctx goctx.Context, + vcVM *object.VirtualMachine, + ecMap map[string]string) (string, error) { + deviceList, err := vcVM.Device(ctx) + if err != nil { + return "", err + } + + var diskData []VMDiskData + for _, device := range deviceList.SelectByType((*types.VirtualDisk)(nil)) { + if disk, ok := device.(*types.VirtualDisk); ok { + vmDiskData := VMDiskData{} + if disk.VDiskId != nil { + vmDiskData.VDiskID = disk.VDiskId.Id + } + if b, ok := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo); ok { + vmDiskData.FileName = b.FileName + } + // Only add the disk data if it's not empty. + if vmDiskData != (VMDiskData{}) { + diskData = append(diskData, vmDiskData) + } + } + } + + diskDataJSON, err := json.Marshal(diskData) + if err != nil { + return "", err + } + diskDataBackup, err := util.EncodeGzipBase64(string(diskDataJSON)) + if err != nil { + return "", err + } + + // Return an empty string to skip the backup if the data is unchanged. + if diskDataBackup == ecMap[constants.BackupVMDiskDataExtraConfigKey] { + return "", nil + } + + return diskDataBackup, nil +} diff --git a/pkg/vmprovider/providers/vsphere2/virtualmachine/backup_test.go b/pkg/vmprovider/providers/vsphere2/virtualmachine/backup_test.go new file mode 100644 index 000000000..d26190ae3 --- /dev/null +++ b/pkg/vmprovider/providers/vsphere2/virtualmachine/backup_test.go @@ -0,0 +1,229 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package virtualmachine_test + +import ( + "encoding/json" + + "sigs.k8s.io/yaml" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" + "github.com/vmware-tanzu/vm-operator/pkg/context" + "github.com/vmware-tanzu/vm-operator/pkg/util" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/constants" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/virtualmachine" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +func backupTests() { + var ( + ctx *builder.TestContextForVCSim + vcVM *object.VirtualMachine + vmCtx context.VirtualMachineContextA2 + ) + + BeforeEach(func() { + ctx = suite.NewTestContextForVCSim(builder.VCSimTestConfig{}) + + var err error + vcVM, err = ctx.Finder.VirtualMachine(ctx, "DC0_C0_RP0_VM0") + Expect(err).NotTo(HaveOccurred()) + + vmCtx = context.VirtualMachineContextA2{ + Context: ctx, + Logger: suite.GetLogger().WithValues("vmName", vcVM.Name()), + VM: &vmopv1.VirtualMachine{}, + } + }) + + AfterEach(func() { + ctx.AfterEach() + ctx = nil + }) + + Context("VM Kube data", func() { + BeforeEach(func() { + vmCtx.VM = builder.DummyVirtualMachineA2() + }) + + When("VM kube data exists in ExtraConfig but is not up-to-date", func() { + + BeforeEach(func() { + oldVM := vmCtx.VM.DeepCopy() + oldVM.ObjectMeta.Generation = 1 + oldVMYaml, err := yaml.Marshal(oldVM) + Expect(err).NotTo(HaveOccurred()) + backupVMYamlEncoded, err := util.EncodeGzipBase64(string(oldVMYaml)) + Expect(err).NotTo(HaveOccurred()) + + _, err = vcVM.Reconfigure(vmCtx, types.VirtualMachineConfigSpec{ + ExtraConfig: []types.BaseOptionValue{ + &types.OptionValue{ + Key: constants.BackupVMKubeDataExtraConfigKey, + Value: backupVMYamlEncoded, + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should backup VM kube data YAML with the latest spec", func() { + vmCtx.VM.ObjectMeta.Generation = 2 + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, nil)).To(Succeed()) + + vmCopy := vmCtx.VM.DeepCopy() + vmCopy.Status = vmopv1.VirtualMachineStatus{} + vmCopyYaml, err := yaml.Marshal(vmCopy) + Expect(err).NotTo(HaveOccurred()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMKubeDataExtraConfigKey, string(vmCopyYaml)) + }) + }) + + When("VM kube data exists in ExtraConfig and is up-to-date", func() { + var ( + kubeDataBackup = "" + ) + + BeforeEach(func() { + vmYaml, err := yaml.Marshal(vmCtx.VM) + Expect(err).NotTo(HaveOccurred()) + kubeDataBackup = string(vmYaml) + encodedKubeDataBackup, err := util.EncodeGzipBase64(kubeDataBackup) + Expect(err).NotTo(HaveOccurred()) + + _, err = vcVM.Reconfigure(vmCtx, types.VirtualMachineConfigSpec{ + ExtraConfig: []types.BaseOptionValue{ + &types.OptionValue{ + Key: constants.BackupVMKubeDataExtraConfigKey, + Value: encodedKubeDataBackup, + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should skip backing up VM kube data", func() { + // Update the VM to verify its kube data is not backed up in ExtraConfig. + vmCtx.VM.Labels = map[string]string{"foo": "bar"} + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, nil)).To(Succeed()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMKubeDataExtraConfigKey, kubeDataBackup) + }) + }) + }) + + Context("VM bootstrap data", func() { + + It("Should back up bootstrap data as JSON in ExtraConfig", func() { + bootstrapDataRaw := map[string]string{"foo": "bar"} + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, bootstrapDataRaw)).To(Succeed()) + + bootstrapDataJSON, err := json.Marshal(bootstrapDataRaw) + Expect(err).NotTo(HaveOccurred()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMBootstrapDataExtraConfigKey, string(bootstrapDataJSON)) + }) + }) + + Context("VM Disk data", func() { + + It("Should backup VM disk data as JSON in ExtraConfig", func() { + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, nil)).To(Succeed()) + + // Use the default disk info from the vcSim VM for testing. + diskData := []virtualmachine.VMDiskData{ + { + VDiskID: "", + FileName: "[LocalDS_0] DC0_C0_RP0_VM0/disk1.vmdk", + }, + } + diskDataJSON, err := json.Marshal(diskData) + Expect(err).NotTo(HaveOccurred()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMDiskDataExtraConfigKey, string(diskDataJSON)) + }) + }) + + Context("VM cloud-init instance ID data", func() { + + BeforeEach(func() { + vmCtx.VM = builder.DummyVirtualMachineA2() + }) + + When("VM cloud-init instance ID already exists in ExtraConfig", func() { + + BeforeEach(func() { + _, err := vcVM.Reconfigure(vmCtx, types.VirtualMachineConfigSpec{ + ExtraConfig: []types.BaseOptionValue{ + &types.OptionValue{ + Key: constants.BackupVMCloudInitInstanceIDExtraConfigKey, + Value: "ec-instance-id", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + vmCtx.VM.Annotations = map[string]string{ + vmopv1.InstanceIDAnnotation: "other-instance-id", + } + }) + + It("Should skip backing up the cloud-init instance ID", func() { + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, nil)).To(Succeed()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMCloudInitInstanceIDExtraConfigKey, "ec-instance-id") + }) + }) + + When("VM cloud-init instance ID does not exist in ExtraConfig and is set in annotations", func() { + + BeforeEach(func() { + vmCtx.VM.Annotations = map[string]string{ + vmopv1.InstanceIDAnnotation: "annotation-instance-id", + } + vmCtx.VM.UID = "vm-uid" + }) + + It("Should backup the cloud-init instance ID from annotations", func() { + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, nil)).To(Succeed()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMCloudInitInstanceIDExtraConfigKey, "annotation-instance-id") + }) + }) + + When("VM cloud-init instance ID does not exist in ExtraConfig and is not set in annotations", func() { + + BeforeEach(func() { + vmCtx.VM.Annotations = nil + vmCtx.VM.UID = "vm-uid" + }) + + It("Should backup the cloud-init instance ID from annotations", func() { + Expect(virtualmachine.BackupVirtualMachine(vmCtx, vcVM, nil)).To(Succeed()) + verifyBackupDataInExtraConfig(ctx, vcVM, constants.BackupVMCloudInitInstanceIDExtraConfigKey, "vm-uid") + }) + }) + }) +} + +func verifyBackupDataInExtraConfig( + ctx *builder.TestContextForVCSim, + vcVM *object.VirtualMachine, + expectedKey, expectedValDecoded string) { + + // Get the VM's ExtraConfig and convert it to map. + moID := vcVM.Reference().Value + objVM := ctx.GetVMFromMoID(moID) + Expect(objVM).NotTo(BeNil()) + var moVM mo.VirtualMachine + Expect(objVM.Properties(ctx, objVM.Reference(), []string{"config.extraConfig"}, &moVM)).To(Succeed()) + ecMap := util.ExtraConfigToMap(moVM.Config.ExtraConfig) + + // Verify the expected key exists in ExtraConfig and the decoded values match. + Expect(ecMap).To(HaveKey(expectedKey)) + ecValRaw := ecMap[expectedKey] + ecValDecoded, err := util.TryToDecodeBase64Gzip([]byte(ecValRaw)) + Expect(err).NotTo(HaveOccurred()) + Expect(ecValDecoded).To(Equal(expectedValDecoded)) +} diff --git a/pkg/vmprovider/providers/vsphere2/virtualmachine/virtualmachine_suite_test.go b/pkg/vmprovider/providers/vsphere2/virtualmachine/virtualmachine_suite_test.go index 12c5c35fd..b78aa26e1 100644 --- a/pkg/vmprovider/providers/vsphere2/virtualmachine/virtualmachine_suite_test.go +++ b/pkg/vmprovider/providers/vsphere2/virtualmachine/virtualmachine_suite_test.go @@ -15,6 +15,7 @@ func vcSimTests() { Describe("ClusterComputeResource", ccrTests) Describe("Delete", deleteTests) Describe("Publish", publishTests) + Describe("Backup", backupTests) } var suite = builder.NewTestSuite() diff --git a/pkg/vmprovider/providers/vsphere2/vmprovider_vm.go b/pkg/vmprovider/providers/vsphere2/vmprovider_vm.go index 1ef1c92a5..97cf1e489 100644 --- a/pkg/vmprovider/providers/vsphere2/vmprovider_vm.go +++ b/pkg/vmprovider/providers/vsphere2/vmprovider_vm.go @@ -167,12 +167,6 @@ func (vs *vSphereVMProvider) PublishVirtualMachine( return itemID, nil } -// BackupVirtualMachine backs up the VM data required for restore. -func (vs *vSphereVMProvider) BackupVirtualMachine(ctx goctx.Context, vm *vmopv1.VirtualMachine) error { - // TODO - return nil -} - func (vs *vSphereVMProvider) GetVirtualMachineGuestHeartbeat( ctx goctx.Context, vm *vmopv1.VirtualMachine) (vmopv1.GuestHeartbeatStatus, error) { @@ -364,6 +358,19 @@ func (vs *vSphereVMProvider) updateVirtualMachine( } } + // Back up the VM at the end after a successful update. + if lib.IsVMServiceBackupRestoreFSSEnabled() { + vmCtx.Logger.V(4).Info("Backing up VirtualMachine") + // TODO: Support backing up vAppConfig bootstrap data. + data, _, _, err := GetVirtualMachineBootstrap(vmCtx, vs.k8sClient) + if err != nil { + return err + } + if err := virtualmachine.BackupVirtualMachine(vmCtx, vcVM, data); err != nil { + return err + } + } + return nil }