Skip to content

Commit

Permalink
Add VM backup implementation for v1a2
Browse files Browse the repository at this point in the history
  • Loading branch information
dilyar85 committed Oct 5, 2023
1 parent be66239 commit 12a9c52
Show file tree
Hide file tree
Showing 10 changed files with 507 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
14 changes: 14 additions & 0 deletions pkg/util/configspec.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,17 @@ func AppendNewExtraConfigValues(

return append(extraConfig, newExtraConfig...)
}

// ExtraConfigToMap converts the ExtraConfig to a map with string values.
func ExtraConfigToMap(input []vimTypes.BaseOptionValue) (output map[string]string) {
output = make(map[string]string)
for _, opt := range input {
if optValue := opt.GetOptionValue(); optValue != nil {
// Only set string type values
if val, ok := optValue.Value.(string); ok {
output[optValue.Key] = val
}
}
}
return
}
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"
// BackupVMInstanceIDExtraConfigKey is the ExtraConfig key to the VM's
// Cloud-Init instance ID, compressed using gzip and base64-encoded.
BackupVMInstanceIDExtraConfigKey = "vmservice.virtualmachine.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/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"
res "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/resources"
)

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 {

resVM := res.NewVMFromObject(vcVM)
moVM, err := resVM.GetProperties(vmCtx, []string{"config", "runtime"})
if 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 := getVMKubeDataBackup(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 := getInstanceIDBackup(vmCtx.VM, curEcMap)
if err != nil {
vmCtx.Logger.Error(err, "Failed to get VM instance ID for backup")
return err
}
if instanceIDBackup == "" {
vmCtx.Logger.V(4).Info("Skipping VM instance ID backup as it exists")
} else {
ecToUpdate = append(ecToUpdate, &types.OptionValue{
Key: constants.BackupVMInstanceIDExtraConfigKey,
Value: instanceIDBackup,
})
}

bootstrapDataBackup, err := getBootstrapDataBackup(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 := getDiskDataBackup(vmCtx, resVM, 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.V(4).Info("Updating VM ExtraConfig with backup data")
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 getVMKubeDataBackup(
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 {
curBackupVM, err := constructVMObj(ecKubeData)
if err != nil {
return "", err
}
if curBackupVM.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 getInstanceIDBackup(
vm *vmopv1.VirtualMachine,
ecMap map[string]string) (string, error) {
// Instance ID should not be changed once persisted in VM's ExtraConfig.
// Return an empty string to skip the backup if it already exists.
if _, ok := ecMap[constants.BackupVMInstanceIDExtraConfigKey]; ok {
return "", nil
}

instanceID := vm.Annotations[vmopv1.InstanceIDAnnotation]
if instanceID == "" {
instanceID = string(vm.UID)
}

return util.EncodeGzipBase64(instanceID)
}

func getBootstrapDataBackup(
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 getDiskDataBackup(
ctx goctx.Context,
resVM *res.VirtualMachine,
ecMap map[string]string) (string, error) {
disks, err := resVM.GetVirtualDisks(ctx)
if err != nil {
return "", err
}

diskData := []VMDiskData{}
for _, device := range disks {
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 12a9c52

Please sign in to comment.