diff --git a/pkg/plugin/vm_backup_item_action.go b/pkg/plugin/vm_backup_item_action.go index 94642b23..7a7b8948 100644 --- a/pkg/plugin/vm_backup_item_action.go +++ b/pkg/plugin/vm_backup_item_action.go @@ -41,12 +41,6 @@ type VMBackupItemAction struct { log logrus.FieldLogger } -const ( - // MetadataBackupLabel indicates that the object will be backed up for metadata purposes. - // This allows skipping restore and consistency-specific checks while ensuring the object is backed up. - MetadataBackupLabel = "velero.kubevirt.io/metadataBackup" -) - // NewVMBackupItemAction instantiates a VMBackupItemAction. func NewVMBackupItemAction(log logrus.FieldLogger) *VMBackupItemAction { return &VMBackupItemAction{log: log} @@ -89,7 +83,7 @@ func (p *VMBackupItemAction) Execute(item runtime.Unstructured, backup *v1.Backu // we can skip all checks that ensure consistency // if we just want to backup for metadata purposes - if !metav1.HasLabel(backup.ObjectMeta, MetadataBackupLabel) { + if !util.IsMetadataBackup(backup) { skipVolume := func(volume kvcore.Volume) bool { return volumeInDVTemplates(volume, vm) } @@ -157,7 +151,7 @@ var isVMIExcludedByLabel = func(vm *kvcore.VirtualMachine) (bool, error) { return false, nil } - label, ok := labels[util.VELERO_EXCLUDE_LABEL] + label, ok := labels[util.VeleroExcludeLabel] return ok && label == "true", nil } diff --git a/pkg/plugin/vm_restore_item_action.go b/pkg/plugin/vm_restore_item_action.go index 90bede72..6475abf8 100644 --- a/pkg/plugin/vm_restore_item_action.go +++ b/pkg/plugin/vm_restore_item_action.go @@ -27,7 +27,9 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/velero" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" kvcore "kubevirt.io/api/core/v1" + "kubevirt.io/kubevirt-velero-plugin/pkg/util" "kubevirt.io/kubevirt-velero-plugin/pkg/util/kvgraph" ) @@ -63,6 +65,12 @@ func (p *VMRestorePlugin) Execute(input *velero.RestoreItemActionExecuteInput) ( return nil, errors.WithStack(err) } + if runStrategy, ok := util.GetRestoreRunStrategy(input.Restore); ok { + p.log.Infof("Setting virtual machine run strategy to %s", runStrategy) + vm.Spec.RunStrategy = ptr.To(runStrategy) + vm.Spec.Running = nil + } + item, err := runtime.DefaultUnstructuredConverter.ToUnstructured(vm) if err != nil { return nil, errors.WithStack(err) diff --git a/pkg/plugin/vm_restore_item_action_test.go b/pkg/plugin/vm_restore_item_action_test.go index 87e2e12f..7527ca9c 100644 --- a/pkg/plugin/vm_restore_item_action_test.go +++ b/pkg/plugin/vm_restore_item_action_test.go @@ -5,7 +5,9 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/plugin/velero" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -19,7 +21,7 @@ func TestVmRestoreExecute(t *testing.T) { "name": "test-vm", }, "spec": map[string]interface{}{ - "running": true, + "runStrategy": "Always", "dataVolumeTemplates": []map[string]interface{}{ {"metadata": map[string]interface{}{ "name": "test-dv-1", @@ -49,6 +51,15 @@ func TestVmRestoreExecute(t *testing.T) { }, }, }, + Restore: &velerov1.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-restore", + Namespace: "default", + }, + Spec: velerov1.RestoreSpec{ + IncludedNamespaces: []string{"default"}, + }, + }, } logrus.SetLevel(logrus.InfoLevel) @@ -58,17 +69,52 @@ func TestVmRestoreExecute(t *testing.T) { assert.Nil(t, err) spec := output.UpdatedItem.UnstructuredContent()["spec"].(map[string]interface{}) - assert.Equal(t, true, spec["running"]) + assert.Equal(t, "Always", spec["runStrategy"]) }) t.Run("Stopped VM should be restored stopped", func(t *testing.T) { spec := input.Item.UnstructuredContent()["spec"].(map[string]interface{}) - spec["running"] = false + spec["runStrategy"] = "Halted" + output, err := action.Execute(&input) + assert.Nil(t, err) + + spec = output.UpdatedItem.UnstructuredContent()["spec"].(map[string]interface{}) + assert.Equal(t, "Halted", spec["runStrategy"]) + }) + + t.Run("Stopped VM should be restored running when using appropriate label", func(t *testing.T) { + spec := input.Item.UnstructuredContent()["spec"].(map[string]interface{}) + spec["runStrategy"] = "Halted" + input.Restore.Labels = map[string]string{"velero.kubevirt.io/restore-run-strategy": "Always"} + output, err := action.Execute(&input) + assert.Nil(t, err) + + spec = output.UpdatedItem.UnstructuredContent()["spec"].(map[string]interface{}) + assert.Equal(t, "Always", spec["runStrategy"]) + }) + + t.Run("Running VM should be restored stopped when using appropriate label", func(t *testing.T) { + spec := input.Item.UnstructuredContent()["spec"].(map[string]interface{}) + spec["runStrategy"] = "Always" + input.Restore.Labels = map[string]string{"velero.kubevirt.io/restore-run-strategy": "Halted"} + output, err := action.Execute(&input) + assert.Nil(t, err) + + spec = output.UpdatedItem.UnstructuredContent()["spec"].(map[string]interface{}) + assert.Equal(t, "Halted", spec["runStrategy"]) + }) + + t.Run("Running field should be cleared when run strategy annotation", func(t *testing.T) { + spec := input.Item.UnstructuredContent()["spec"].(map[string]interface{}) + spec["running"] = true + spec["runStrategy"] = "" + input.Restore.Labels = map[string]string{"velero.kubevirt.io/restore-run-strategy": "Halted"} output, err := action.Execute(&input) assert.Nil(t, err) spec = output.UpdatedItem.UnstructuredContent()["spec"].(map[string]interface{}) - assert.Equal(t, false, spec["running"]) + assert.Equal(t, "Halted", spec["runStrategy"]) + assert.Nil(t, spec["running"]) }) t.Run("VM should return DVs as additional items", func(t *testing.T) { diff --git a/pkg/plugin/vmi_backup_item_action.go b/pkg/plugin/vmi_backup_item_action.go index f4fdc154..2a9deeb0 100644 --- a/pkg/plugin/vmi_backup_item_action.go +++ b/pkg/plugin/vmi_backup_item_action.go @@ -105,7 +105,7 @@ func (p *VMIBackupItemAction) Execute(item runtime.Unstructured, backup *v1.Back if isVMIOwned(vmi) { util.AddAnnotation(item, AnnIsOwned, "true") - } else if !metav1.HasLabel(backup.ObjectMeta, MetadataBackupLabel) { + } else if !util.IsMetadataBackup(backup) { restore, err := util.RestorePossible(vmi.Spec.Volumes, backup, vmi.Namespace, func(volume kvcore.Volume) bool { return false }, p.log) if err != nil { return nil, nil, errors.WithStack(err) @@ -156,7 +156,7 @@ var isVMExcludedByLabel = func(vmi *kvcore.VirtualMachineInstance) (bool, error) return false, err } - label, ok := vm.GetLabels()[util.VELERO_EXCLUDE_LABEL] + label, ok := vm.GetLabels()[util.VeleroExcludeLabel] return ok && label == "true", nil } @@ -174,6 +174,6 @@ func (p *VMIBackupItemAction) isPodExcludedByLabel(vmi *kvcore.VirtualMachineIns return false, nil } - label, ok := labels[util.VELERO_EXCLUDE_LABEL] + label, ok := labels[util.VeleroExcludeLabel] return ok && label == "true", nil } diff --git a/pkg/util/util.go b/pkg/util/util.go index 36152b12..4203f21c 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -24,7 +24,17 @@ import ( cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" ) -const VELERO_EXCLUDE_LABEL = "velero.io/exclude-from-backup" +const ( + // MetadataBackupLabel indicates that the object will be backed up for metadata purposes. + // This allows skipping restore and consistency-specific checks while ensuring the object is backed up. + MetadataBackupLabel = "velero.kubevirt.io/metadataBackup" + + // RestoreRunStrategy indicates that the backed up VMs will be powered with the specified run strategy after restore. + RestoreRunStrategy = "velero.kubevirt.io/restore-run-strategy" + + // VeleroExcludeLabel is used to exclude an object from Velero backups. + VeleroExcludeLabel = "velero.io/exclude-from-backup" +) func GetK8sClient() (*kubernetes.Clientset, error) { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() @@ -209,7 +219,7 @@ var IsDVExcludedByLabel = func(namespace, dvName string) (bool, error) { return false, nil } - label, ok := labels[VELERO_EXCLUDE_LABEL] + label, ok := labels[VeleroExcludeLabel] return ok && label == "true", nil } @@ -230,7 +240,7 @@ var IsPVCExcludedByLabel = func(namespace, pvcName string) (bool, error) { return false, nil } - label, ok := labels[VELERO_EXCLUDE_LABEL] + label, ok := labels[VeleroExcludeLabel] return ok && label == "true", nil } @@ -307,3 +317,14 @@ func getNamespaceAndNetworkName(vmiNamespace, fullNetworkName string) (string, s } return vmiNamespace, fullNetworkName } + +func GetRestoreRunStrategy(restore *velerov1.Restore) (kvv1.VirtualMachineRunStrategy, bool) { + if metav1.HasLabel(restore.ObjectMeta, RestoreRunStrategy) { + return kvv1.VirtualMachineRunStrategy(restore.Labels[RestoreRunStrategy]), true + } + return "", false +} + +func IsMetadataBackup(backup *velerov1.Backup) bool { + return metav1.HasLabel(backup.ObjectMeta, MetadataBackupLabel) +} diff --git a/tests/framework/backup.go b/tests/framework/backup.go index 6e2ead20..ec4f6b4e 100644 --- a/tests/framework/backup.go +++ b/tests/framework/backup.go @@ -189,7 +189,7 @@ func CreateSnapshotLocation(ctx context.Context, locationName, provider, region return nil } -func CreateRestoreForBackup(ctx context.Context, backupName, restoreName string, backupNamespace string, wait bool) error { +func CreateRestoreWithLabels(ctx context.Context, backupName, restoreName, backupNamespace string, wait bool, labels map[string]string) error { args := []string{ "restore", "create", restoreName, "--from-backup", backupName, @@ -199,6 +199,13 @@ func CreateRestoreForBackup(ctx context.Context, backupName, restoreName string, if wait { args = append(args, "--wait") } + if len(labels) > 0 { + labelPairs := []string{} + for key, value := range labels { + labelPairs = append(labelPairs, fmt.Sprintf("%s=%s", key, value)) + } + args = append(args, "--labels", strings.Join(labelPairs, ",")) + } restoreCmd := exec.CommandContext(ctx, veleroCLI, args...) restoreCmd.Stdout = os.Stdout @@ -212,6 +219,10 @@ func CreateRestoreForBackup(ctx context.Context, backupName, restoreName string, return nil } +func CreateRestoreForBackup(ctx context.Context, backupName, restoreName, backupNamespace string, wait bool) error { + return CreateRestoreWithLabels(ctx, backupName, restoreName, backupNamespace, wait, nil) +} + func GetRestore(ctx context.Context, restoreName string, backupNamespace string) (*v1.Restore, error) { checkCMD := exec.CommandContext(ctx, veleroCLI, "restore", "get", "-n", backupNamespace, "-o", "json", restoreName) diff --git a/tests/vm_backup_test.go b/tests/vm_backup_test.go index cc93e816..0f8df280 100644 --- a/tests/vm_backup_test.go +++ b/tests/vm_backup_test.go @@ -199,6 +199,67 @@ var _ = Describe("[smoke] VM Backup", func() { Expect(err).ToNot(HaveOccurred()) }) + DescribeTable("should respect power state configuration after restore", func(startVM bool, restoreLabel map[string]string, expectedState kvv1.VirtualMachinePrintableStatus) { + By("Creating a VM") + var err error + vm, err = framework.CreateStartedVirtualMachine(f.KvClient, f.Namespace.Name, framework.CreateVmWithGuestAgent("test-vm", f.StorageClass)) + Expect(err).ToNot(HaveOccurred()) + + err = framework.WaitForVirtualMachineStatus(f.KvClient, f.Namespace.Name, vm.Name, kvv1.VirtualMachineStatusRunning) + Expect(err).ToNot(HaveOccurred()) + + if !startVM { + By("Stopping VM") + err = framework.StopVirtualMachine(f.KvClient, f.Namespace.Name, vm.Name) + Expect(err).ToNot(HaveOccurred()) + err = framework.WaitForVirtualMachineStatus(f.KvClient, f.Namespace.Name, vm.Name, kvv1.VirtualMachineStatusStopped) + Expect(err).ToNot(HaveOccurred()) + } + + By("Creating a backup for the VM") + err = framework.CreateBackupForNamespace(timeout, backupName, f.Namespace.Name, snapshotLocation, f.BackupNamespace, true) + Expect(err).ToNot(HaveOccurred()) + + phase, err := framework.GetBackupPhase(timeout, backupName, f.BackupNamespace) + Expect(err).ToNot(HaveOccurred()) + Expect(phase).To(Equal(velerov1api.BackupPhaseCompleted)) + + if startVM { + By("Stopping VM") + err = framework.StopVirtualMachine(f.KvClient, f.Namespace.Name, vm.Name) + Expect(err).ToNot(HaveOccurred()) + err = framework.WaitForVirtualMachineStatus(f.KvClient, f.Namespace.Name, vm.Name, kvv1.VirtualMachineStatusStopped) + Expect(err).ToNot(HaveOccurred()) + } + + By("Deleting the VM") + err = framework.DeleteVirtualMachine(f.KvClient, f.Namespace.Name, vm.Name) + Expect(err).ToNot(HaveOccurred()) + + By("Creating restore with specific label") + err = framework.CreateRestoreWithLabels(timeout, backupName, restoreName, f.BackupNamespace, true, restoreLabel) + Expect(err).ToNot(HaveOccurred()) + + rPhase, err := framework.GetRestorePhase(timeout, restoreName, f.BackupNamespace) + Expect(err).ToNot(HaveOccurred()) + Expect(rPhase).To(Equal(velerov1api.RestorePhaseCompleted)) + + By("Validating the restored VM state") + err = framework.WaitForVirtualMachineStatus(f.KvClient, f.Namespace.Name, vm.Name, expectedState) + Expect(err).ToNot(HaveOccurred()) + }, + Entry("Restore with Always run strategy label should start the VM", + false, + map[string]string{"velero.kubevirt.io/restore-run-strategy": "Always"}, + kvv1.VirtualMachineStatusRunning, + ), + Entry("Restore with Halted run strategy label should stop the VM", + true, + map[string]string{"velero.kubevirt.io/restore-run-strategy": "Halted"}, + kvv1.VirtualMachineStatusStopped, + ), + ) + Context("VM and VMI object graph backup", func() { Context("with instancetypes and preferences", func() { nsDelFunc := func() {