diff --git a/images/virtualization-artifact/pkg/builder/cvi/option.go b/images/virtualization-artifact/pkg/builder/cvi/option.go index 718dff2ff7..8401859dc7 100644 --- a/images/virtualization-artifact/pkg/builder/cvi/option.go +++ b/images/virtualization-artifact/pkg/builder/cvi/option.go @@ -80,6 +80,17 @@ func WithDatasource(datasource v1alpha2.ClusterVirtualImageDataSource) func(cvi } } +func WithDataSourceHTTPWithOnlyURL(url string) Option { + return func(cvi *v1alpha2.ClusterVirtualImage) { + cvi.Spec.DataSource = v1alpha2.ClusterVirtualImageDataSource{ + Type: v1alpha2.DataSourceTypeHTTP, + HTTP: &v1alpha2.DataSourceHTTP{ + URL: url, + }, + } + } +} + func WithPhase(phase v1alpha2.ImagePhase) func(cvi *v1alpha2.ClusterVirtualImage) { return func(cvi *v1alpha2.ClusterVirtualImage) { cvi.Status.Phase = phase diff --git a/images/virtualization-artifact/pkg/builder/vd/option.go b/images/virtualization-artifact/pkg/builder/vd/option.go index 36817f8277..2e89707bb5 100644 --- a/images/virtualization-artifact/pkg/builder/vd/option.go +++ b/images/virtualization-artifact/pkg/builder/vd/option.go @@ -55,6 +55,17 @@ func WithDataSourceHTTP(url string, checksum *v1alpha2.Checksum, caBundle []byte } } +func WithDataSourceHTTPWithOnlyURL(url string) Option { + return func(vd *v1alpha2.VirtualDisk) { + vd.Spec.DataSource = &v1alpha2.VirtualDiskDataSource{ + Type: v1alpha2.DataSourceTypeHTTP, + HTTP: &v1alpha2.DataSourceHTTP{ + URL: url, + }, + } + } +} + func WithDataSourceContainerImage(image, imagePullSecretName string, caBundle []byte) Option { return func(vd *v1alpha2.VirtualDisk) { vd.Spec.DataSource = &v1alpha2.VirtualDiskDataSource{ diff --git a/images/virtualization-artifact/pkg/builder/vi/option.go b/images/virtualization-artifact/pkg/builder/vi/option.go index 411f552ddb..1881c840b5 100644 --- a/images/virtualization-artifact/pkg/builder/vi/option.go +++ b/images/virtualization-artifact/pkg/builder/vi/option.go @@ -91,3 +91,20 @@ func WithStorage(storage v1alpha2.StorageType) func(vi *v1alpha2.VirtualImage) { vi.Spec.Storage = storage } } + +func WithStorageType(storageType *v1alpha2.StorageType) func(vi *v1alpha2.VirtualImage) { + return func(vi *v1alpha2.VirtualImage) { + vi.Spec.Storage = *storageType + } +} + +func WithDataSourceHTTPWithOnlyURL(url string) Option { + return func(vi *v1alpha2.VirtualImage) { + vi.Spec.DataSource = v1alpha2.VirtualImageDataSource{ + Type: v1alpha2.DataSourceTypeHTTP, + HTTP: &v1alpha2.DataSourceHTTP{ + URL: url, + }, + } + } +} diff --git a/images/virtualization-artifact/pkg/builder/vmbda/option.go b/images/virtualization-artifact/pkg/builder/vmbda/option.go new file mode 100644 index 0000000000..284cc6d126 --- /dev/null +++ b/images/virtualization-artifact/pkg/builder/vmbda/option.go @@ -0,0 +1,42 @@ +/* +Copyright 2025 Flant JSC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vmbda + +import ( + "github.com/deckhouse/virtualization-controller/pkg/builder/meta" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Option func(vd *v1alpha2.VirtualMachineBlockDeviceAttachment) + +var ( + WithName = meta.WithName[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithNamespace = meta.WithNamespace[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithLabel = meta.WithLabel[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithLabels = meta.WithLabels[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithAnnotation = meta.WithAnnotation[*v1alpha2.VirtualMachineBlockDeviceAttachment] + WithAnnotations = meta.WithAnnotations[*v1alpha2.VirtualMachineBlockDeviceAttachment] +) + +func WithBlockDeviceRef(bdRef v1alpha2.VMBDAObjectRef) func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + return func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + vmbda.Spec.BlockDeviceRef = bdRef + } +} + +func WithVMName(vmName string) func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + return func(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment) { + vmbda.Spec.VirtualMachineName = vmName + } +} diff --git a/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go b/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go new file mode 100644 index 0000000000..c0600566aa --- /dev/null +++ b/images/virtualization-artifact/pkg/builder/vmbda/vmbda.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 Flant JSC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vmbda + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func New(options ...Option) *v1alpha2.VirtualMachineBlockDeviceAttachment { + vmbda := NewEmpty("", "") + ApplyOptions(vmbda, options) + return vmbda +} + +func ApplyOptions(vmbda *v1alpha2.VirtualMachineBlockDeviceAttachment, opts []Option) { + if vmbda == nil { + return + } + for _, opt := range opts { + opt(vmbda) + } +} + +func NewEmpty(name, namespace string) *v1alpha2.VirtualMachineBlockDeviceAttachment { + return &v1alpha2.VirtualMachineBlockDeviceAttachment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualMachineBlockDeviceAttachmentKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} diff --git a/images/virtualization-artifact/pkg/builder/vmop/option.go b/images/virtualization-artifact/pkg/builder/vmop/option.go index 22e38dad9d..9ebfdc81ad 100644 --- a/images/virtualization-artifact/pkg/builder/vmop/option.go +++ b/images/virtualization-artifact/pkg/builder/vmop/option.go @@ -51,3 +51,9 @@ func WithForce(force *bool) Option { vmop.Spec.Force = force } } + +func WithRestoreSpec(restoreSpec *v1alpha2.VirtualMachineOperationRestoreSpec) Option { + return func(vmop *v1alpha2.VirtualMachineOperation) { + vmop.Spec.Restore = restoreSpec + } +} diff --git a/images/virtualization-artifact/pkg/builder/vmsnapshot/option.go b/images/virtualization-artifact/pkg/builder/vmsnapshot/option.go new file mode 100644 index 0000000000..e9a367cd6c --- /dev/null +++ b/images/virtualization-artifact/pkg/builder/vmsnapshot/option.go @@ -0,0 +1,53 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vmsnapshot + +import ( + "github.com/deckhouse/virtualization-controller/pkg/builder/meta" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Option func(vmop *v1alpha2.VirtualMachineSnapshot) + +var ( + WithName = meta.WithName[*v1alpha2.VirtualMachineSnapshot] + WithGenerateName = meta.WithGenerateName[*v1alpha2.VirtualMachineSnapshot] + WithNamespace = meta.WithNamespace[*v1alpha2.VirtualMachineSnapshot] + WithLabel = meta.WithLabel[*v1alpha2.VirtualMachineSnapshot] + WithLabels = meta.WithLabels[*v1alpha2.VirtualMachineSnapshot] + WithAnnotation = meta.WithAnnotation[*v1alpha2.VirtualMachineSnapshot] + WithAnnotations = meta.WithAnnotations[*v1alpha2.VirtualMachineSnapshot] + WithFinalizer = meta.WithFinalizer[*v1alpha2.VirtualMachineSnapshot] +) + +func WithRequiredConsistency(required bool) Option { + return func(vmop *v1alpha2.VirtualMachineSnapshot) { + vmop.Spec.RequiredConsistency = required + } +} + +func WithVM(vmName string) Option { + return func(vmop *v1alpha2.VirtualMachineSnapshot) { + vmop.Spec.VirtualMachineName = vmName + } +} + +func WithKeepIPAddress(keepIPAddress v1alpha2.KeepIPAddress) Option { + return func(vmop *v1alpha2.VirtualMachineSnapshot) { + vmop.Spec.KeepIPAddress = keepIPAddress + } +} diff --git a/images/virtualization-artifact/pkg/builder/vmsnapshot/vmsnapshot.go b/images/virtualization-artifact/pkg/builder/vmsnapshot/vmsnapshot.go new file mode 100644 index 0000000000..804f5bfd6c --- /dev/null +++ b/images/virtualization-artifact/pkg/builder/vmsnapshot/vmsnapshot.go @@ -0,0 +1,51 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vmsnapshot + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func New(options ...Option) *v1alpha2.VirtualMachineSnapshot { + vmSnapshot := NewEmpty("", "") + ApplyOptions(vmSnapshot, options...) + return vmSnapshot +} + +func ApplyOptions(vmSnapshot *v1alpha2.VirtualMachineSnapshot, opts ...Option) { + if vmSnapshot == nil { + return + } + for _, opt := range opts { + opt(vmSnapshot) + } +} + +func NewEmpty(name, namespace string) *v1alpha2.VirtualMachineSnapshot { + return &v1alpha2.VirtualMachineSnapshot{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualMachineSnapshotKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} diff --git a/tests/e2e/framework/framework.go b/tests/e2e/framework/framework.go index df5cf13300..252f193f0a 100644 --- a/tests/e2e/framework/framework.go +++ b/tests/e2e/framework/framework.go @@ -22,6 +22,7 @@ import ( "maps" "sync" + "github.com/deckhouse/virtualization/tests/e2e/config" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -83,13 +84,16 @@ func (f *Framework) Before() { gomega.Expect(err).NotTo(gomega.HaveOccurred()) ginkgo.By(fmt.Sprintf("Created namespace %s", ns.Name)) f.namespace = ns - f.DeferNamespaceDelete(ns.Name) } } func (f *Framework) After() { ginkgo.GinkgoHelper() + if !config.IsCleanUpNeeded() { + return + } + for _, obj := range f.objectsToDelete { ginkgo.By(fmt.Sprintf("Delete object %s", obj.GetName())) err := f.GenericClient().Delete(context.Background(), obj) diff --git a/tests/e2e/virtual_machine_restore_operation_test/cloud_init.go b/tests/e2e/virtual_machine_restore_operation_test/cloud_init.go new file mode 100644 index 0000000000..91f2f1ca22 --- /dev/null +++ b/tests/e2e/virtual_machine_restore_operation_test/cloud_init.go @@ -0,0 +1,34 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package virtualmachinerestoreoperationtest + +const cloudInit = `#cloud-config +users: +- name: cloud + passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i. + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + chpasswd: { expire: False } + lock_passwd: false + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com + +runcmd: +- [bash, -c, "apt update"] +- [bash, -c, "apt install qemu-guest-agent -y"] +- [bash, -c, "systemctl enable qemu-guest-agent"] +- [bash, -c, "systemctl start qemu-guest-agent"]` diff --git a/tests/e2e/virtual_machine_restore_operation_test/vmop_restore_test_helper.go b/tests/e2e/virtual_machine_restore_operation_test/vmop_restore_test_helper.go new file mode 100644 index 0000000000..d0b0018662 --- /dev/null +++ b/tests/e2e/virtual_machine_restore_operation_test/vmop_restore_test_helper.go @@ -0,0 +1,373 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package virtualmachinerestoreoperationtest + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + + cvibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/cvi" + vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vi" + vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" + vmbdabuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmbda" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/tests/e2e/framework" +) + +const ( + ubuntuURL = "https://89d64382-20df-4581-8cc7-80df331f67fa.selstorage.ru/ubuntu/jammy-minimal-cloudimg-amd64.img" + viURL = "https://89d64382-20df-4581-8cc7-80df331f67fa.selstorage.ru/test/test.qcow2" + cviURL = "https://89d64382-20df-4581-8cc7-80df331f67fa.selstorage.ru/test/test.iso" + defaultValue = "value" + changedValue = "changed" + testAnnotationName = "test-annotation" + testLabelName = "test-label" +) + +type VMOPRestoreTestHelper struct { + FrameworkEntity *framework.Framework + VM *v1alpha2.VirtualMachine + CVI *v1alpha2.ClusterVirtualImage + VI *v1alpha2.VirtualImage + VDRoot, VDBlank *v1alpha2.VirtualDisk + VMBDA *v1alpha2.VirtualMachineBlockDeviceAttachment + VMSnapshot *v1alpha2.VirtualMachineSnapshot + VMOPDryRun *v1alpha2.VirtualMachineOperation + VMOPStrict *v1alpha2.VirtualMachineOperation + VMOPBestEffort *v1alpha2.VirtualMachineOperation + GeneratedValue string + RunningLastTransitionTime time.Time +} + +func NewVMOPRestoreTestHelper(frameworkEntity *framework.Framework) *VMOPRestoreTestHelper { + return &VMOPRestoreTestHelper{ + FrameworkEntity: frameworkEntity, + } +} + +func (h *VMOPRestoreTestHelper) GenerateAndCreateOriginalResources() { + GinkgoHelper() + h.CVI = cvibuilder.New( + cvibuilder.WithGenerateName("ubuntu-cvi-"), + cvibuilder.WithDataSourceHTTPWithOnlyURL(cviURL), + ) + + // for getting real cvi name + err := h.FrameworkEntity.GenericClient().Create(context.Background(), h.CVI) + By(fmt.Sprintf("Created cvi: %s", h.CVI.Name)) + Expect(err).ShouldNot(HaveOccurred()) + + h.FrameworkEntity.DeferDelete(h.CVI) + h.VI = vibuilder.New( + vibuilder.WithName("ubuntu-vi"), + vibuilder.WithNamespace(h.FrameworkEntity.Namespace().Name), + vibuilder.WithDataSourceHTTPWithOnlyURL(viURL), + vibuilder.WithStorageType(ptr.To(v1alpha2.StoragePersistentVolumeClaim)), + ) + h.VDRoot = vdbuilder.New( + vdbuilder.WithName("vd-root"), + vdbuilder.WithNamespace(h.FrameworkEntity.Namespace().Name), + vdbuilder.WithSize(ptr.To(resource.MustParse("10Gi"))), + vdbuilder.WithDataSourceHTTPWithOnlyURL(ubuntuURL), + ) + h.VDBlank = vdbuilder.New( + vdbuilder.WithName("vd-blank"), + vdbuilder.WithNamespace(h.FrameworkEntity.Namespace().Name), + vdbuilder.WithSize(ptr.To(resource.MustParse("51Mi"))), + ) + h.VM = vmbuilder.New( + vmbuilder.WithAnnotation(h.GetTestAnnotationName(), h.GetDefaultValue()), + vmbuilder.WithLabel(h.GetTestLabelName(), h.GetDefaultValue()), + vmbuilder.WithName("ubuntu-vm"), + vmbuilder.WithNamespace(h.FrameworkEntity.Namespace().Name), + vmbuilder.WithLiveMigrationPolicy(v1alpha2.AlwaysSafeMigrationPolicy), + vmbuilder.WithCPU(1, ptr.To("10%")), + vmbuilder.WithMemory(resource.MustParse("1Gi")), + vmbuilder.WithProvisioning( + &v1alpha2.Provisioning{ + Type: v1alpha2.ProvisioningTypeUserData, + UserData: cloudInit, + }, + ), + vmbuilder.WithBlockDeviceRefs( + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.DiskDevice, + Name: h.VDRoot.Name, + }, + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.ClusterImageDevice, + Name: h.CVI.Name, + }, + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.ImageDevice, + Name: h.VI.Name, + }, + ), + ) + h.VMBDA = vmbdabuilder.New( + vmbdabuilder.WithName("vmbda"), + vmbdabuilder.WithNamespace(h.FrameworkEntity.Namespace().Name), + vmbdabuilder.WithVMName(h.VM.Name), + vmbdabuilder.WithBlockDeviceRef(v1alpha2.VMBDAObjectRef{ + Kind: v1alpha2.VMBDAObjectRefKindVirtualDisk, + Name: h.VDBlank.Name, + }), + ) + + By(fmt.Sprintf("Creating vi: %s/%s", h.VI.Namespace, h.VI.Name)) + err = h.FrameworkEntity.GenericClient().Create(context.Background(), h.VI) + Expect(err).ShouldNot(HaveOccurred()) + By(fmt.Sprintf("Creating vd blank: %s/%s", h.VDBlank.Namespace, h.VDBlank.Name)) + err = h.FrameworkEntity.GenericClient().Create(context.Background(), h.VDBlank) + Expect(err).ShouldNot(HaveOccurred()) + By(fmt.Sprintf("Creating vd root: %s/%s", h.VDRoot.Namespace, h.VDRoot.Name)) + err = h.FrameworkEntity.GenericClient().Create(context.Background(), h.VDRoot) + Expect(err).ShouldNot(HaveOccurred()) + By(fmt.Sprintf("Creating vm: %s/%s", h.VM.Namespace, h.VM.Name)) + err = h.FrameworkEntity.GenericClient().Create(context.Background(), h.VM) + Expect(err).ShouldNot(HaveOccurred()) + By(fmt.Sprintf("Creating vmbda: %s/%s", h.VMBDA.Namespace, h.VMBDA.Name)) + err = h.FrameworkEntity.GenericClient().Create(context.Background(), h.VMBDA) + Expect(err).ShouldNot(HaveOccurred()) +} + +func (h *VMOPRestoreTestHelper) UpdateState() { + GinkgoHelper() + + var err error + + if h.CVI != nil { + var cvi v1alpha2.ClusterVirtualImage + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Name: h.CVI.Name, + }, + &cvi, + ) + if err == nil { + h.CVI = &cvi + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VI != nil { + var vi v1alpha2.VirtualImage + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VI.Namespace, + Name: h.VI.Name, + }, + &vi, + ) + if err == nil { + h.VI = &vi + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VDBlank != nil { + var vdBlank v1alpha2.VirtualDisk + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VDBlank.Namespace, + Name: h.VDBlank.Name, + }, + &vdBlank, + ) + if err == nil { + h.VDBlank = &vdBlank + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VDRoot != nil { + var vdRoot v1alpha2.VirtualDisk + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VDRoot.Namespace, + Name: h.VDRoot.Name, + }, + &vdRoot, + ) + if err == nil { + h.VDRoot = &vdRoot + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VMBDA != nil { + var vmbda v1alpha2.VirtualMachineBlockDeviceAttachment + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VMBDA.Namespace, + Name: h.VMBDA.Name, + }, + &vmbda, + ) + if err == nil { + h.VMBDA = &vmbda + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VM != nil { + var vm v1alpha2.VirtualMachine + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VM.Namespace, + Name: h.VM.Name, + }, + &vm, + ) + if err == nil { + h.VM = &vm + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VMSnapshot != nil { + var vmSnapshot v1alpha2.VirtualMachineSnapshot + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VMSnapshot.Namespace, + Name: h.VMSnapshot.Name, + }, + &vmSnapshot, + ) + if err == nil { + h.VMSnapshot = &vmSnapshot + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VMOPDryRun != nil { + var vmopDryRun v1alpha2.VirtualMachineOperation + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VMOPDryRun.Namespace, + Name: h.VMOPDryRun.Name, + }, + &vmopDryRun, + ) + if err == nil { + h.VMOPDryRun = &vmopDryRun + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VMOPStrict != nil { + var vmopStrict v1alpha2.VirtualMachineOperation + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VMOPStrict.Namespace, + Name: h.VMOPStrict.Name, + }, + &vmopStrict, + ) + if err == nil { + h.VMOPStrict = &vmopStrict + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } + + if h.VMOPBestEffort != nil { + var vmopBestEffort v1alpha2.VirtualMachineOperation + err = h.FrameworkEntity.Clients.GenericClient().Get( + context.Background(), + types.NamespacedName{ + Namespace: h.VMOPBestEffort.Namespace, + Name: h.VMOPBestEffort.Name, + }, + &vmopBestEffort, + ) + if err == nil { + h.VMOPBestEffort = &vmopBestEffort + } else { + Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + } + } +} + +func (h *VMOPRestoreTestHelper) CheckIfResourcesReady(g Gomega) { + g.Expect(h.CVI.Status.Phase).Should(Equal(v1alpha2.ImageReady)) + g.Expect(h.VI.Status.Phase).Should(Equal(v1alpha2.ImageReady)) + g.Expect(h.VDBlank.Status.Phase).Should(Equal(v1alpha2.DiskReady)) + g.Expect(h.VDRoot.Status.Phase).Should(Equal(v1alpha2.DiskReady)) + g.Expect(h.VMBDA.Status.Phase).Should(Equal(v1alpha2.BlockDeviceAttachmentPhaseAttached)) + g.Expect(h.VM.Status.Phase).Should(Equal(v1alpha2.MachineRunning)) + + agentReady, _ := conditions.GetCondition(vmcondition.TypeAgentReady, h.VM.Status.Conditions) + g.Expect(agentReady.Status).Should(Equal(metav1.ConditionTrue)) +} + +func (h *VMOPRestoreTestHelper) GetDefaultValue() string { + return defaultValue +} + +func (h *VMOPRestoreTestHelper) GetChangedValue() string { + return changedValue +} + +func (h *VMOPRestoreTestHelper) GetTestAnnotationName() string { + return testAnnotationName +} + +func (h *VMOPRestoreTestHelper) GetTestLabelName() string { + return testLabelName +} + +func (h *VMOPRestoreTestHelper) CreateFsAndSetValueOnDiskShell(value string) string { + return fmt.Sprintf("umount /mnt &>/dev/null || DEV=/dev/$(sudo lsblk | grep disk | tail -n 1 | awk \"{print \\$1}\") && sudo mkfs.ext4 $DEV && sudo mount $DEV /mnt && sudo bash -c \"echo %s > /mnt/value\"", value) +} + +func (h *VMOPRestoreTestHelper) ChangeValueOnDiskShell(value string) string { + return fmt.Sprintf("sudo bash -c \"echo %s > /mnt/value\"", value) +} + +func (h *VMOPRestoreTestHelper) MountAndGetDiskFileContentShell() string { + return "umount /mnt &>/dev/null || DEV=/dev/$(sudo lsblk | grep disk | tail -n 1 | awk \"{print \\$1}\") && sudo mount $DEV /mnt && cat /mnt/value" +} diff --git a/tests/e2e/vmop_restore_test.go b/tests/e2e/vmop_restore_test.go new file mode 100644 index 0000000000..c9fff7add4 --- /dev/null +++ b/tests/e2e/vmop_restore_test.go @@ -0,0 +1,480 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "context" + "fmt" + "strconv" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + vmbdabuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmbda" + vmopbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmop" + vmsnapshotbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vmsnapshot" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmopcondition" + "github.com/deckhouse/virtualization/tests/e2e/d8" + "github.com/deckhouse/virtualization/tests/e2e/framework" + virtualmachinerestoreoperationtest "github.com/deckhouse/virtualization/tests/e2e/virtual_machine_restore_operation_test" +) + +var _ = Describe("VirtualMachineOperationRestore", Serial, framework.CommonE2ETestDecorators(), func() { + frameworkEntity := framework.NewFramework("virtual-machine-operation-restore") + helper := virtualmachinerestoreoperationtest.NewVMOPRestoreTestHelper(frameworkEntity) + + frameworkEntity.BeforeAll() + frameworkEntity.AfterAll() + + Context("Preparing resources", func() { + It("Applying resources", func() { + helper.GenerateAndCreateOriginalResources() + }) + + It("Resources should be ready", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + helper.CheckIfResourcesReady(g) + }, 600*time.Second, 1*time.Second).Should(Succeed()) + }) + + It("Creates file on the last disk", func() { + Eventually(func(g Gomega) { + helper.GeneratedValue = strconv.Itoa(time.Now().UTC().Second()) + + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, helper.CreateFsAndSetValueOnDiskShell(helper.GeneratedValue), d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + }, 10*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Creating snapshot", func() { + It("Applying snapshot resource", func() { + helper.VMSnapshot = vmsnapshotbuilder.New( + vmsnapshotbuilder.WithName("vmsnapshot"), + vmsnapshotbuilder.WithNamespace(frameworkEntity.Namespace().Name), + vmsnapshotbuilder.WithVM(helper.VM.Name), + vmsnapshotbuilder.WithRequiredConsistency(true), + vmsnapshotbuilder.WithKeepIPAddress(v1alpha2.KeepIPAddressAlways), + ) + By(fmt.Sprintf("Creating vm snapshot: %s/%s", helper.VMSnapshot.Namespace, helper.VMSnapshot.Name)) + err := frameworkEntity.Clients.GenericClient().Create(context.Background(), helper.VMSnapshot) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("Snapshot should be Ready", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + + g.Expect(helper.VMSnapshot.Status.Phase).Should(Equal(v1alpha2.VirtualMachineSnapshotPhaseReady)) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Changing VM", func() { + It("Change data of created file", func() { + Eventually(func(g Gomega) { + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, helper.ChangeValueOnDiskShell(helper.GetChangedValue()), d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + }, 10*time.Second, time.Second).Should(Succeed()) + }) + + It("Change VM spec", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + + helper.VM.Annotations[helper.GetTestAnnotationName()] = helper.GetChangedValue() + helper.VM.Labels[helper.GetTestLabelName()] = helper.GetChangedValue() + helper.VM.Spec.CPU.Cores = 2 + helper.VM.Spec.Memory.Size = resource.MustParse("2Gi") + + err := helper.FrameworkEntity.Clients.GenericClient().Update(context.Background(), helper.VM) + g.Expect(err).ShouldNot(HaveOccurred()) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("Reboot VM", func() { + running, _ := conditions.GetCondition(vmcondition.TypeRunning, helper.VM.Status.Conditions) + helper.RunningLastTransitionTime = running.LastTransitionTime.Time + + Eventually(func(g Gomega) { + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, "sudo reboot", d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + }, 10*time.Second, time.Second).Should(Succeed()) + + Eventually(func(g Gomega) { + helper.UpdateState() + + running, _ := conditions.GetCondition(vmcondition.TypeRunning, helper.VM.Status.Conditions) + g.Expect(running.LastTransitionTime.Time.After(helper.RunningLastTransitionTime)).Should(BeTrue()) + + agentReady, _ := conditions.GetCondition(vmcondition.TypeAgentReady, helper.VM.Status.Conditions) + g.Expect(agentReady.Status).Should(Equal(metav1.ConditionTrue)) + }, 120*time.Second, time.Second).Should(Succeed()) + }) + + It("VM spec should be changed", func() { + Expect(helper.VM.Annotations[helper.GetTestAnnotationName()]).Should(Equal(helper.GetChangedValue())) + Expect(helper.VM.Labels[helper.GetTestLabelName()]).Should(Equal(helper.GetChangedValue())) + Expect(helper.VM.Spec.CPU.Cores).Should(Equal(2)) + Expect(helper.VM.Spec.Memory.Size).Should(Equal(resource.MustParse("2Gi"))) + }) + }) + + Context("Restore DryRun", func() { + It("Applying DryRun restore VMOP", func() { + helper.VMOPDryRun = vmopbuilder.New( + vmopbuilder.WithName("vmop-dryrun"), + vmopbuilder.WithNamespace(helper.FrameworkEntity.Namespace().Name), + vmopbuilder.WithVirtualMachine(helper.VM.Name), + vmopbuilder.WithType(v1alpha2.VMOPTypeRestore), + vmopbuilder.WithRestoreSpec(&v1alpha2.VirtualMachineOperationRestoreSpec{ + VirtualMachineSnapshotName: helper.VMSnapshot.Name, + Mode: v1alpha2.VMOPRestoreModeDryRun, + }), + ) + err := frameworkEntity.Clients.GenericClient().Create(context.Background(), helper.VMOPDryRun) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("VMOP should be Completed", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + + g.Expect(helper.VMOPDryRun.Status.Phase).Should(Equal(v1alpha2.VMOPPhaseCompleted)) + + restoreCompleted, _ := conditions.GetCondition(vmopcondition.TypeRestoreCompleted, helper.VMOPDryRun.Status.Conditions) + g.Expect(restoreCompleted.Status).Should(Equal(metav1.ConditionTrue)) + g.Expect(restoreCompleted.Reason).Should(Equal(vmopcondition.ReasonDryRunOperationCompleted.String())) + g.Expect(restoreCompleted.Message).Should(Equal("The virtual machine can be restored from the snapshot.")) + }, 120*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Check VM state", func() { + It("VM should have changed state", func() { + helper.UpdateState() + Expect(helper.VM.Annotations[helper.GetTestAnnotationName()]).Should(Equal(helper.GetChangedValue())) + Expect(helper.VM.Labels[helper.GetTestLabelName()]).Should(Equal(helper.GetChangedValue())) + Expect(helper.VM.Spec.CPU.Cores).Should(Equal(2)) + Expect(helper.VM.Spec.Memory.Size).Should(Equal(resource.MustParse("2Gi"))) + }) + }) + + Context("Restore BestEffort", func() { + It("Applying BestEffort restore VMOP", func() { + helper.VMOPBestEffort = vmopbuilder.New( + vmopbuilder.WithName("vmop-best-effort"), + vmopbuilder.WithNamespace(helper.FrameworkEntity.Namespace().Name), + vmopbuilder.WithVirtualMachine(helper.VM.Name), + vmopbuilder.WithType(v1alpha2.VMOPTypeRestore), + vmopbuilder.WithRestoreSpec(&v1alpha2.VirtualMachineOperationRestoreSpec{ + VirtualMachineSnapshotName: helper.VMSnapshot.Name, + Mode: v1alpha2.VMOPRestoreModeBestEffort, + }), + ) + err := frameworkEntity.Clients.GenericClient().Create(context.Background(), helper.VMOPBestEffort) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("VMOP should be Completed", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + + g.Expect(helper.VMOPBestEffort.Status.Phase).Should(Equal(v1alpha2.VMOPPhaseCompleted)) + }, 120*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Check VM state", func() { + It("VM should have restored state", func() { + helper.UpdateState() + Expect(helper.VM.Annotations[helper.GetTestAnnotationName()]).Should(Equal(helper.GetDefaultValue())) + Expect(helper.VM.Labels[helper.GetTestLabelName()]).Should(Equal(helper.GetDefaultValue())) + Expect(helper.VM.Spec.CPU.Cores).Should(Equal(1)) + Expect(helper.VM.Spec.Memory.Size).Should(Equal(resource.MustParse("1Gi"))) + }) + + // It will be removed in the future: once the bug is fixed that causes a virtual machine to remain in the Stopped phase after recovery in BestEffort mode. + It("Seems like a bug, wait and start VM", func() { + time.Sleep(20 * time.Second) + helper.UpdateState() + + if helper.VM.Status.Phase == v1alpha2.MachineStopped { + By("Forcing VM start") + + startVMOP := vmopbuilder.New( + vmopbuilder.WithName("start"), + vmopbuilder.WithNamespace(helper.FrameworkEntity.Namespace().Name), + vmopbuilder.WithVirtualMachine(helper.VM.Name), + vmopbuilder.WithType(v1alpha2.VMOPTypeStart), + ) + err := helper.FrameworkEntity.Clients.GenericClient().Create(context.Background(), startVMOP) + Expect(err).ShouldNot(HaveOccurred()) + } + }) + + It("Virtual Machine agent should be ready", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + agentReady, _ := conditions.GetCondition(vmcondition.TypeAgentReady, helper.VM.Status.Conditions) + g.Expect(agentReady.Status).Should(Equal(metav1.ConditionTrue)) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("VMBDA should be attached", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + g.Expect(helper.VMBDA.Status.Phase).Should(Equal(v1alpha2.BlockDeviceAttachmentPhaseAttached)) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("File should have generated value", func() { + Eventually(func(g Gomega) { + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, helper.MountAndGetDiskFileContentShell(), d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + + g.Expect(res.StdOut()).Should(ContainSubstring(helper.GeneratedValue)) + }, 10*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Changing VM", func() { + It("Change data of created file", func() { + Eventually(func(g Gomega) { + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, helper.ChangeValueOnDiskShell("removed"), d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + }, 10*time.Second, time.Second).Should(Succeed()) + }) + + It("Change VM spec", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + + helper.VM.Annotations[helper.GetTestAnnotationName()] = helper.GetChangedValue() + helper.VM.Labels[helper.GetTestLabelName()] = helper.GetChangedValue() + helper.VM.Spec.CPU.Cores = 2 + helper.VM.Spec.Memory.Size = resource.MustParse("2Gi") + + err := helper.FrameworkEntity.Clients.GenericClient().Update(context.Background(), helper.VM) + g.Expect(err).ShouldNot(HaveOccurred()) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("Reboot VM", func() { + running, _ := conditions.GetCondition(vmcondition.TypeRunning, helper.VM.Status.Conditions) + helper.RunningLastTransitionTime = running.LastTransitionTime.Time + + Eventually(func(g Gomega) { + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, "sudo reboot", d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + }, 10*time.Second, time.Second).Should(Succeed()) + + Eventually(func(g Gomega) { + helper.UpdateState() + + running, _ := conditions.GetCondition(vmcondition.TypeRunning, helper.VM.Status.Conditions) + g.Expect(running.LastTransitionTime.Time.After(helper.RunningLastTransitionTime)).Should(BeTrue()) + + agentReady, _ := conditions.GetCondition(vmcondition.TypeAgentReady, helper.VM.Status.Conditions) + g.Expect(agentReady.Status).Should(Equal(metav1.ConditionTrue)) + }, 120*time.Second, time.Second).Should(Succeed()) + }) + + It("VM spec should be changed", func() { + helper.UpdateState() + Expect(helper.VM.Annotations[helper.GetTestAnnotationName()]).Should(Equal(helper.GetChangedValue())) + Expect(helper.VM.Labels[helper.GetTestLabelName()]).Should(Equal(helper.GetChangedValue())) + Expect(helper.VM.Spec.CPU.Cores).Should(Equal(2)) + Expect(helper.VM.Spec.Memory.Size).Should(Equal(resource.MustParse("2Gi"))) + }) + + It("Shutdown VM", func() { + Eventually(func(g Gomega) { + framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, "sudo poweroff", d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + + helper.UpdateState() + g.Expect(helper.VM.Status.Phase).Should(Equal(v1alpha2.MachineStopped)) + }, 10*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Removing resources", func() { + It("Delete resources", func() { + err := helper.FrameworkEntity.Clients.GenericClient().Delete(context.Background(), helper.VMBDA) + Expect(err).ShouldNot(HaveOccurred()) + err = helper.FrameworkEntity.Clients.GenericClient().Delete(context.Background(), helper.VDBlank) + Expect(err).ShouldNot(HaveOccurred()) + err = helper.FrameworkEntity.Clients.GenericClient().Delete(context.Background(), helper.VDRoot) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("Resources should be deleted", func() { + Eventually(func(g Gomega) { + var vmbda v1alpha2.VirtualMachineBlockDeviceAttachment + err := helper.FrameworkEntity.Clients.GenericClient().Get(context.Background(), types.NamespacedName{ + Namespace: helper.VMBDA.Namespace, + Name: helper.VMBDA.Name, + }, &vmbda) + g.Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + + var vdRoot v1alpha2.VirtualDisk + err = helper.FrameworkEntity.Clients.GenericClient().Get(context.Background(), types.NamespacedName{ + Namespace: helper.VDRoot.Namespace, + Name: helper.VDRoot.Name, + }, &vdRoot) + g.Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + + var vdBlank v1alpha2.VirtualDisk + err = helper.FrameworkEntity.Clients.GenericClient().Get(context.Background(), types.NamespacedName{ + Namespace: helper.VDBlank.Namespace, + Name: helper.VDBlank.Name, + }, &vdBlank) + g.Expect(k8serrors.IsNotFound(err)).Should(BeTrue()) + }, 300*time.Second, time.Second).Should(Succeed()) + }) + }) + + Context("Restore Strict", func() { + It("Applying Strict restore VMOP", func() { + helper.VMOPStrict = vmopbuilder.New( + vmopbuilder.WithName("vmop-strict"), + vmopbuilder.WithNamespace(helper.FrameworkEntity.Namespace().Name), + vmopbuilder.WithVirtualMachine(helper.VM.Name), + vmopbuilder.WithType(v1alpha2.VMOPTypeRestore), + vmopbuilder.WithRestoreSpec(&v1alpha2.VirtualMachineOperationRestoreSpec{ + VirtualMachineSnapshotName: helper.VMSnapshot.Name, + Mode: v1alpha2.VMOPRestoreModeStrict, + }), + ) + err := frameworkEntity.Clients.GenericClient().Create(context.Background(), helper.VMOPStrict) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("VMOP should be Completed", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + + g.Expect(helper.VMOPStrict.Status.Phase).Should(Equal(v1alpha2.VMOPPhaseCompleted)) + }, 60*time.Second, 1*time.Second).Should(Succeed()) + }) + + // It will be removed in the future: we need to fix the bug that prevents VMBDA from being restored. + It("Recreate VMBDA", func() { + helper.VMBDA = vmbdabuilder.New( + vmbdabuilder.WithName("vmbda"), + vmbdabuilder.WithNamespace(helper.FrameworkEntity.Namespace().Name), + vmbdabuilder.WithVMName(helper.VM.Name), + vmbdabuilder.WithBlockDeviceRef(v1alpha2.VMBDAObjectRef{ + Kind: v1alpha2.VMBDAObjectRefKindVirtualDisk, + Name: helper.VDBlank.Name, + }), + ) + err := frameworkEntity.Clients.GenericClient().Create(context.Background(), helper.VMBDA) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("Waiting VMBDA", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + g.Expect(helper.VMBDA.Status.Phase).Should(Equal(v1alpha2.BlockDeviceAttachmentPhaseAttached)) + }, 120*time.Second, time.Second) + }) + }) + + Context("Check VM state", func() { + It("VM agent should be ready", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + agentReady, _ := conditions.GetCondition(vmcondition.TypeAgentReady, helper.VM.Status.Conditions) + g.Expect(agentReady.Status).Should(Equal(metav1.ConditionTrue)) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("VM should have restored state", func() { + helper.UpdateState() + Expect(helper.VM.Annotations[helper.GetTestAnnotationName()]).Should(Equal(helper.GetDefaultValue())) + Expect(helper.VM.Labels[helper.GetTestLabelName()]).Should(Equal(helper.GetDefaultValue())) + Expect(helper.VM.Spec.CPU.Cores).Should(Equal(1)) + Expect(helper.VM.Spec.Memory.Size).Should(Equal(resource.MustParse("1Gi"))) + }) + + It("Virtual Machine agent should be ready", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + agentReady, _ := conditions.GetCondition(vmcondition.TypeAgentReady, helper.VM.Status.Conditions) + g.Expect(agentReady.Status).Should(Equal(metav1.ConditionTrue)) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("VMBDA should be attached", func() { + Eventually(func(g Gomega) { + helper.UpdateState() + g.Expect(helper.VMBDA.Status.Phase).Should(Equal(v1alpha2.BlockDeviceAttachmentPhaseAttached)) + }, 60*time.Second, time.Second).Should(Succeed()) + }) + + It("File should have generated value", func() { + Eventually(func(g Gomega) { + res := framework.GetClients().D8Virtualization().SSHCommand(helper.VM.Name, helper.MountAndGetDiskFileContentShell(), d8.SSHOptions{ + Namespace: helper.VM.Namespace, + Username: conf.TestData.SSHUser, + IdentityFile: conf.TestData.Sshkey, + }) + g.Expect(res.Error()).ShouldNot(HaveOccurred()) + + g.Expect(res.StdOut()).Should(ContainSubstring(helper.GeneratedValue)) + }, 10*time.Second, time.Second).Should(Succeed()) + }) + }) +})