Skip to content

Commit

Permalink
Allow switching VM power state during restore (#306)
Browse files Browse the repository at this point in the history
* Allow specifying VM RunStrategy before restore

This commit implements a new label that allows switching the VM RunStrategy before restore.

Signed-off-by: Alvaro Romero <[email protected]>

* Add functional test coverage

This commit adds func test coverage for the "velero.kubevirt.io/restore-power-on" and "velero.kubevirt.io/restore-power-off" labels.

Signed-off-by: Alvaro Romero <[email protected]>

---------

Signed-off-by: Alvaro Romero <[email protected]>
  • Loading branch information
alromeros authored Dec 17, 2024
1 parent 150eb7b commit d6abb26
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 19 deletions.
10 changes: 2 additions & 8 deletions pkg/plugin/vm_backup_item_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/plugin/vm_restore_item_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down
54 changes: 50 additions & 4 deletions pkg/plugin/vm_restore_item_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/plugin/vmi_backup_item_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
27 changes: 24 additions & 3 deletions pkg/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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)
}
13 changes: 12 additions & 1 deletion tests/framework/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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)

Expand Down
61 changes: 61 additions & 0 deletions tests/vm_backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit d6abb26

Please sign in to comment.