Skip to content

Commit

Permalink
🌱 Add VM backup implementation for v1a2 (#240)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dilyar85 authored Oct 18, 2023
1 parent c7be07a commit b5dda1b
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package v1alpha2_test
import (
"context"
"errors"
"os"
"strings"

. "github.com/onsi/ginkgo"
Expand All @@ -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"
Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 0 additions & 12 deletions pkg/vmprovider/fake/fake_vm_provider_a2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
1 change: 0 additions & 1 deletion pkg/vmprovider/interface_a2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions pkg/vmprovider/providers/vsphere2/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
237 changes: 237 additions & 0 deletions pkg/vmprovider/providers/vsphere2/virtualmachine/backup.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit b5dda1b

Please sign in to comment.