diff --git a/test/e2e/default_config.yaml b/test/e2e/default_config.yaml index cca1b5f139..1d4b361723 100644 --- a/test/e2e/default_config.yaml +++ b/test/e2e/default_config.yaml @@ -18,7 +18,6 @@ clusterTransport: helperImages: curlImage: "curlimages/curl" testData: - diskResizing: "/tmp/testdata/disk-resizing" imageHotplug: "/tmp/testdata/image-hotplug" sizingPolicy: "/tmp/testdata/sizing-policy" vmConfiguration: "/tmp/testdata/vm-configuration" diff --git a/test/e2e/internal/config/config.go b/test/e2e/internal/config/config.go index 7cd7078b9c..f4a792d99f 100644 --- a/test/e2e/internal/config/config.go +++ b/test/e2e/internal/config/config.go @@ -88,7 +88,6 @@ type Config struct { } type TestData struct { - DiskResizing string `yaml:"diskResizing"` ImageHotplug string `yaml:"imageHotplug"` VMMigration string `yaml:"vmMigration"` VMMigrationCancel string `yaml:"vmMigrationCancel"` diff --git a/test/e2e/internal/util/block_device.go b/test/e2e/internal/util/block_device.go index e57a4786c5..496628bb04 100644 --- a/test/e2e/internal/util/block_device.go +++ b/test/e2e/internal/util/block_device.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -105,6 +106,26 @@ func GetBlockDeviceHash(ctx context.Context, f *framework.Framework, vm *v1alpha return strings.TrimSpace(cmdOut) } +func GetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm *v1alpha2.VirtualMachine, bdKind v1alpha2.BlockDeviceKind, bdName string) resource.Quantity { + GinkgoHelper() + + serial, ok := GetBlockDeviceSerialNumber(ctx, vm, bdKind, bdName) + Expect(ok).To(BeTrue(), "failed to get block device serial number") + + devicePath, err := GetBlockDeviceBySerial(f, vm, serial) + Expect(err).NotTo(HaveOccurred(), "failed to get device by serial") + + cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --json -o SIZE %s", devicePath)) + Expect(err).NotTo(HaveOccurred()) + + var disks Disks + err = json.Unmarshal([]byte(cmdOut), &disks) + Expect(err).NotTo(HaveOccurred(), "failed to parse lsblk output") + Expect(disks.BlockDevices).NotTo(BeEmpty(), "lsblk output does not contain block devices") + + return resource.MustParse(strings.TrimSpace(disks.BlockDevices[0].Size)) +} + func GetBlockDeviceBySerial(f *framework.Framework, vm *v1alpha2.VirtualMachine, serial string) (string, error) { cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk -o PATH,SERIAL | grep %s | awk \"{print \\$1, \\$2}\"", serial)) if err != nil { diff --git a/test/e2e/legacy/image_hotplug.go b/test/e2e/legacy/image_hotplug.go index a03e838e85..eeccb42e4e 100644 --- a/test/e2e/legacy/image_hotplug.go +++ b/test/e2e/legacy/image_hotplug.go @@ -38,7 +38,11 @@ import ( "github.com/deckhouse/virtualization/test/e2e/internal/util" ) -const unacceptableCount = -1000 +const ( + DiskIDPrefix = "scsi-0QEMU_QEMU_HARDDISK" + CdRomIDPrefix = "scsi-0QEMU_QEMU_CD-ROM_drive-ua" + unacceptableCount = -1000 +) var APIVersion = v1alpha2.SchemeGroupVersion.String() @@ -416,3 +420,22 @@ func GetDisksMetadata(vmNamespace, vmName string, disks *Disks) error { } return nil } + +func WaitBlockDeviceRefsAttached(namespace string, vms ...string) { + GinkgoHelper() + Eventually(func() error { + for _, vmName := range vms { + vm := v1alpha2.VirtualMachine{} + err := GetObject(v1alpha2.VirtualMachineResource, vmName, &vm, kc.GetOptions{Namespace: namespace}) + if err != nil { + return fmt.Errorf("virtualMachine: %s\nstderr: %w", vmName, err) + } + for _, bd := range vm.Status.BlockDeviceRefs { + if !bd.Attached { + return fmt.Errorf("virtualMachine: %s\nblockDeviceRefs: %#v", vmName, bd) + } + } + } + return nil + }).WithTimeout(Timeout).WithPolling(Interval).Should(Succeed()) +} diff --git a/test/e2e/legacy/testdata/disk-resizing/base/cfg/cloudinit.yaml b/test/e2e/legacy/testdata/disk-resizing/base/cfg/cloudinit.yaml deleted file mode 100644 index 2ec8f0c999..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/base/cfg/cloudinit.yaml +++ /dev/null @@ -1,23 +0,0 @@ -#cloud-config -package_update: true -packages: - - qemu-guest-agent - - curl - - bash - - sudo - - iputils -users: - - name: cloud - # passwd: cloud - passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i. - shell: /bin/bash - sudo: ALL=(ALL) NOPASSWD:ALL - lock_passwd: false - ssh_authorized_keys: - # testcases - - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com -final_message: "\U0001F525\U0001F525\U0001F525 The system is finally up, after ${updame} \U0001F525\U0001F525\U0001F525" -runcmd: - - "echo \"\U0001F7E1 Starting runcmd at $(date +%H:%M:%S)\"" - - "rc-update add qemu-guest-agent && rc-service qemu-guest-agent start" - - "echo \"\U0001F7E1 Finished runcmd at $(date +%H:%M:%S)\"" diff --git a/test/e2e/legacy/testdata/disk-resizing/base/kustomization.yaml b/test/e2e/legacy/testdata/disk-resizing/base/kustomization.yaml deleted file mode 100644 index 8ff06116f3..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/base/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ./vm.yaml - - ./vd-root.yaml - - ./vd-blank.yaml -configurations: - - transformer.yaml -generatorOptions: - disableNameSuffixHash: true -secretGenerator: - - files: - - userData=cfg/cloudinit.yaml - name: cloud-init - type: provisioning.virtualization.deckhouse.io/cloud-init diff --git a/test/e2e/legacy/testdata/disk-resizing/base/transformer.yaml b/test/e2e/legacy/testdata/disk-resizing/base/transformer.yaml deleted file mode 100644 index 1dc146a3af..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/base/transformer.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# https://github.com/kubernetes-sigs/kustomize/blob/master/examples/transformerconfigs/README.md#transformer-configurations - -namespace: - - kind: ClusterVirtualImage - path: spec/dataSource/objectRef/namespace -nameReference: - - kind: VirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: ClusterVirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: VirtualDisk - version: v1alpha2 # optional - fieldSpecs: - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - path: spec/blockDeviceRef/name - kind: VirtualMachineBlockDeviceAttachment - - kind: Secret - fieldSpecs: - - path: spec/provisioning/userDataRef/name - kind: VirtualMachine - - kind: VirtualMachineIPAddress - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineIPAddressName - kind: VirtualMachine - - kind: VirtualMachine - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineName - kind: VirtualMachineBlockDeviceAttachment - - kind: VirtualMachineClass - version: v1alpha3 - fieldSpecs: - - path: spec/virtualMachineClassName - kind: VirtualMachine diff --git a/test/e2e/legacy/testdata/disk-resizing/base/vd-blank.yaml b/test/e2e/legacy/testdata/disk-resizing/base/vd-blank.yaml deleted file mode 100644 index fcc4b94ea8..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/base/vd-blank.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualDisk -metadata: - name: vd-blank -spec: - persistentVolumeClaim: - storageClassName: "{{ .STORAGE_CLASS_NAME }}" - size: 100Mi diff --git a/test/e2e/legacy/testdata/disk-resizing/base/vd-root.yaml b/test/e2e/legacy/testdata/disk-resizing/base/vd-root.yaml deleted file mode 100644 index b4f9cd0c2c..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/base/vd-root.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualDisk -metadata: - name: vd-root -spec: - persistentVolumeClaim: - storageClassName: "{{ .STORAGE_CLASS_NAME }}" - size: 512Mi - dataSource: - type: ObjectRef - objectRef: - kind: ClusterVirtualImage - name: v12n-e2e-alpine-uefi-perf diff --git a/test/e2e/legacy/testdata/disk-resizing/base/vm.yaml b/test/e2e/legacy/testdata/disk-resizing/base/vm.yaml deleted file mode 100644 index 003988d6ca..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/base/vm.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualMachine -metadata: - name: vm -spec: - bootloader: EFI - virtualMachineClassName: generic - cpu: - cores: 1 - coreFraction: 50% - memory: - size: 256Mi - disruptions: - restartApprovalMode: Manual - provisioning: - type: UserDataRef - userDataRef: - kind: Secret - name: cloud-init - blockDeviceRefs: - - kind: VirtualDisk - name: vd-root - - kind: VirtualDisk - name: vd-blank diff --git a/test/e2e/legacy/testdata/disk-resizing/kustomization.yaml b/test/e2e/legacy/testdata/disk-resizing/kustomization.yaml deleted file mode 100644 index a30957df4a..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: testcases -namePrefix: pr-number-or-commit-hash- -resources: - - ns.yaml - - overlays/automatic-with-hotplug -configurations: - - transformer.yaml -labels: - - includeSelectors: true - pairs: - id: pr-number-or-commit-hash - testcase: disk-resizing diff --git a/test/e2e/legacy/testdata/disk-resizing/ns.yaml b/test/e2e/legacy/testdata/disk-resizing/ns.yaml deleted file mode 100644 index 5efde875b6..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/ns.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: default diff --git a/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/kustomization.yaml b/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/kustomization.yaml deleted file mode 100644 index eb682177d5..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/kustomization.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -nameSuffix: -disk-resizing -resources: - - ../../base - - ./vd-attach.yaml - - ./vmbda.yaml -patches: - - patch: |- - - op: replace - path: /spec/runPolicy - value: AlwaysOn - target: - kind: VirtualMachine - name: vm - - patch: |- - - op: replace - path: /spec/disruptions/restartApprovalMode - value: Automatic - target: - kind: VirtualMachine - name: vm -labels: - - includeSelectors: true - pairs: - vm: automatic-with-hotplug-standalone diff --git a/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/vd-attach.yaml b/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/vd-attach.yaml deleted file mode 100644 index a68911289e..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/vd-attach.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualDisk -metadata: - name: vd-attach -spec: - persistentVolumeClaim: - storageClassName: "{{ .STORAGE_CLASS_NAME }}" - size: 100Mi diff --git a/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/vmbda.yaml b/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/vmbda.yaml deleted file mode 100644 index 30a7b6ba4a..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/overlays/automatic-with-hotplug/vmbda.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualMachineBlockDeviceAttachment -metadata: - name: blank-disk-attachment -spec: - virtualMachineName: vm - blockDeviceRef: - kind: VirtualDisk - name: vd-attach diff --git a/test/e2e/legacy/testdata/disk-resizing/transformer.yaml b/test/e2e/legacy/testdata/disk-resizing/transformer.yaml deleted file mode 100644 index ec70d37fcd..0000000000 --- a/test/e2e/legacy/testdata/disk-resizing/transformer.yaml +++ /dev/null @@ -1,52 +0,0 @@ -namespace: - - kind: ClusterVirtualImage - path: spec/dataSource/objectRef/namespace -nameReference: - - kind: VirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: ClusterVirtualImage - version: v1alpha2 # optional - fieldSpecs: - - path: spec/dataSource/objectRef/name - kind: ClusterVirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualImage - - path: spec/dataSource/objectRef/name - kind: VirtualDisk - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - kind: VirtualDisk - version: v1alpha2 # optional - fieldSpecs: - - path: spec/blockDeviceRefs/name - kind: VirtualMachine - - path: spec/blockDeviceRef/name - kind: VirtualMachineBlockDeviceAttachment - - kind: Secret - fieldSpecs: - - path: spec/provisioning/userDataRef/name - kind: VirtualMachine - - kind: VirtualMachineIPAddress - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineIPAddressName - kind: VirtualMachine - - kind: VirtualMachine - version: v1alpha2 - fieldSpecs: - - path: spec/virtualMachineName - kind: VirtualMachineBlockDeviceAttachment - - kind: VirtualMachineClass - version: v1alpha3 - fieldSpecs: - - path: spec/virtualMachineClassName - kind: VirtualMachine diff --git a/test/e2e/legacy/vm_disk_resizing.go b/test/e2e/legacy/vm_disk_resizing.go deleted file mode 100644 index 981b11703b..0000000000 --- a/test/e2e/legacy/vm_disk_resizing.go +++ /dev/null @@ -1,387 +0,0 @@ -/* -Copyright 2024 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 legacy - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - "sync" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/resource" - virtv1 "kubevirt.io/api/core/v1" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/test/e2e/internal/d8" - "github.com/deckhouse/virtualization/test/e2e/internal/framework" - kc "github.com/deckhouse/virtualization/test/e2e/internal/kubectl" - "github.com/deckhouse/virtualization/test/e2e/internal/label" - "github.com/deckhouse/virtualization/test/e2e/internal/precheck" -) - -var _ = Describe("VirtualDiskResizing", Ordered, label.Legacy(), Label(precheck.NoPrecheck), func() { - const ( - vmCount = 1 - diskCount = 3 - ) - var vmObj *v1alpha2.VirtualMachine - var ns string - testCaseLabel := map[string]string{"testcase": "disk-resizing"} - - BeforeAll(func() { - kustomization := fmt.Sprintf("%s/%s", conf.TestData.DiskResizing, "kustomization.yaml") - var err error - ns, err = kustomize.GetNamespace(kustomization) - Expect(err).NotTo(HaveOccurred(), "%w", err) - - CreateNamespace(ns) - }) - - AfterEach(func() { - if CurrentSpecReport().Failed() { - SaveTestCaseDump(testCaseLabel, CurrentSpecReport().LeafNodeText, ns) - } - }) - - AfterAll(func() { - if conf.IsCleanupNeeded { - DeleteTestCaseResources(ns, ResourcesToDelete{ - KustomizationDir: conf.TestData.DiskResizing, - }) - } - }) - - Context("When the resources are applied", func() { - It("result should be succeeded", func() { - res := kubectl.Apply(kc.ApplyOptions{ - Filename: []string{conf.TestData.DiskResizing}, - FilenameOption: kc.Kustomize, - }) - Expect(res.WasSuccess()).To(Equal(true), res.StdErr()) - }) - }) - - Context("When the virtual disks are applied", func() { - It("checks `VirtualDisks` phase", func() { - By(fmt.Sprintf("`VirtualDisks` should be in the %q phases", v1alpha2.DiskReady), func() { - WaitPhaseByLabel(kc.ResourceVD, string(v1alpha2.DiskReady), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - }) - - Context("When the virtual machine are applied", func() { - It("checks `VirtualMachine` phase", func() { - By("`VirtualMachine` agent should be ready", func() { - WaitVMAgentReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - - It("retrieves the test objects", func() { - By("`VirtualMachine`", func() { - vmObjs := &v1alpha2.VirtualMachineList{} - err := GetObjects(v1alpha2.VirtualMachineResource, vmObjs, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - }) - Expect(err).NotTo(HaveOccurred(), "failed to get `VirtualMachines`: %s", err) - Expect(vmObjs.Items).To(HaveLen(vmCount), "there is only %d `VirtualMachine` in this test case", vmCount) - vmObj = &vmObjs.Items[0] - Expect(vmObj).ShouldNot(BeNil(), "failed to retrieve `VirtualMachine` object: %+v", vmObjs) - }) - }) - }) - - Context("When the virtual machine block device attachment is applied", func() { - It("checks `VirtualMachineBlockDeviceAttachment` phase", func() { - By(fmt.Sprintf("`VirtualMachineBlockDeviceAttachment` should be in the %q phases", v1alpha2.BlockDeviceAttachmentPhaseAttached), func() { - WaitPhaseByLabel(kc.ResourceVMBDA, string(v1alpha2.BlockDeviceAttachmentPhaseAttached), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }) - }) - - Describe("Resizing", func() { - Context("When the virtual machine is ready", func() { - var ( - vmDisksBefore VirtualMachineDisks - vmDisksAfter VirtualMachineDisks - err error - ) - - It("obtains the disks metadata before resizing", func() { - vmDisksBefore, err = GetVirtualMachineDisks(ns, vmObj.Name) - Expect(err).NotTo(HaveOccurred()) - Expect(vmDisksBefore).Should(HaveLen(diskCount)) - }) - - It("resizes the disks", func() { - wg := &sync.WaitGroup{} - res := kubectl.List(kc.ResourceVD, kc.GetOptions{ - Labels: testCaseLabel, - Namespace: ns, - Output: "jsonpath='{.items[*].metadata.name}'", - }) - Expect(res.Error()).NotTo(HaveOccurred(), res.StdErr()) - - vds := strings.Split(res.StdOut(), " ") - Expect(vds).Should(HaveLen(diskCount)) - addedSize := resource.NewQuantity(100*1024*1024, resource.BinarySI) - wg.Add(2) - go func() { - defer GinkgoRecover() - defer wg.Done() - By(fmt.Sprintf("`VirtualDisks` should be in the %q phase", v1alpha2.DiskResizing), func() { - WaitPhaseByLabel(v1alpha2.VirtualDiskResource, string(v1alpha2.DiskResizing), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - }() - go func() { - defer GinkgoRecover() - defer wg.Done() - ResizeDisks(addedSize, ns, vds...) - }() - wg.Wait() - }) - - It("checks `VirtualDisks`, `VirtualMachine` and `VirtualMachineBlockDeviceAttachment` phases", func() { - By(fmt.Sprintf("`VirtualDisks` should be in the %q phases", v1alpha2.DiskReady), func() { - WaitPhaseByLabel(kc.ResourceVD, string(v1alpha2.DiskReady), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - By("`VirtualMachine` should be ready", func() { - WaitVMAgentReady(kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - By(fmt.Sprintf("`VirtualMachineBlockDeviceAttachment` should be in the %q phases", v1alpha2.BlockDeviceAttachmentPhaseAttached), func() { - WaitPhaseByLabel(kc.ResourceVMBDA, string(v1alpha2.BlockDeviceAttachmentPhaseAttached), kc.WaitOptions{ - Labels: testCaseLabel, - Namespace: ns, - Timeout: MaxWaitTimeout, - }) - }) - By("`BlockDevices` from the status should be attached", func() { - WaitBlockDeviceRefsAttached(ns, vmObj.Name) - }) - }) - - It("obtains and compares the disks metadata after resizing", func() { - Eventually(func() error { - vmDisksAfter, err = GetVirtualMachineDisks(ns, vmObj.Name) - if err != nil { - return fmt.Errorf("failed to obtain disks metadata after resizing: %w", err) - } - if len(vmDisksAfter) != diskCount { - return fmt.Errorf("quantity of the disk should be %d", diskCount) - } - for disk := range vmDisksBefore { - sizeFromObjectBefore := vmDisksBefore[disk].SizeFromObject.Value() - sizeFromObjectAfter := vmDisksAfter[disk].SizeFromObject.Value() - if sizeFromObjectBefore >= sizeFromObjectAfter { - return fmt.Errorf( - "size from objects before must be lower than size after: before: %d, after: %d", - sizeFromObjectBefore, - sizeFromObjectAfter, - ) - } - sizeByLsblkBefore := vmDisksBefore[disk].SizeByLsblk.Value() - sizeByLsblkAfter := vmDisksAfter[disk].SizeByLsblk.Value() - if sizeByLsblkBefore >= sizeByLsblkAfter { - return fmt.Errorf( - "size by lsblk before must be lower than size after: before: %d, after: %d", - sizeByLsblkBefore, - sizeByLsblkAfter, - ) - } - } - return nil - }).WithTimeout(Timeout).WithPolling(Interval).Should(Succeed()) - }) - }) - }) -}) - -type VirtualMachineDisks map[string]DiskMetaData - -type DiskMetaData struct { - ID string - SizeByLsblk *resource.Quantity - SizeFromObject *resource.Quantity -} - -const ( - DiskIDPrefix = "scsi-0QEMU_QEMU_HARDDISK" - CdRomIDPrefix = "scsi-0QEMU_QEMU_CD-ROM_drive-ua" -) - -func WaitBlockDeviceRefsAttached(namespace string, vms ...string) { - GinkgoHelper() - Eventually(func() error { - for _, vmName := range vms { - vm := v1alpha2.VirtualMachine{} - err := GetObject(v1alpha2.VirtualMachineResource, vmName, &vm, kc.GetOptions{Namespace: namespace}) - if err != nil { - return fmt.Errorf("virtualMachine: %s\nstderr: %w", vmName, err) - } - for _, bd := range vm.Status.BlockDeviceRefs { - if !bd.Attached { - return fmt.Errorf("virtualMachine: %s\nblockDeviceRefs: %#v", vmName, bd) - } - } - } - return nil - }).WithTimeout(Timeout).WithPolling(Interval).Should(Succeed()) -} - -func ResizeDisks(addedSize *resource.Quantity, ns string, virtualDisks ...string) { - GinkgoHelper() - wg := &sync.WaitGroup{} - for _, vd := range virtualDisks { - wg.Add(1) - go func() { - defer GinkgoRecover() - defer wg.Done() - diskObject := v1alpha2.VirtualDisk{} - err := GetObject(kc.ResourceVD, vd, &diskObject, kc.GetOptions{Namespace: ns}) - Expect(err).NotTo(HaveOccurred(), "%v", err) - newValue := resource.NewQuantity(diskObject.Spec.PersistentVolumeClaim.Size.Value()+addedSize.Value(), resource.BinarySI) - mergePatch := fmt.Sprintf("{\"spec\":{\"persistentVolumeClaim\":{\"size\":\"%s\"}}}", newValue.String()) - err = MergePatchResource(kc.ResourceVD, ns, vd, mergePatch) - Expect(err).NotTo(HaveOccurred(), "%v", err) - }() - } - wg.Wait() -} - -func GetSizeFromObject(vdName, namespace string) (*resource.Quantity, error) { - GinkgoHelper() - vd := v1alpha2.VirtualDisk{} - err := GetObject(kc.ResourceVD, vdName, &vd, kc.GetOptions{Namespace: namespace}) - if err != nil { - return nil, err - } - return vd.Spec.PersistentVolumeClaim.Size, nil -} - -func GetSizeByLsblk(vmNamespace, vmName, diskIDPath string) (*resource.Quantity, error) { - GinkgoHelper() - var ( - blockDevices *BlockDevices - quantity resource.Quantity - ) - cmd := fmt.Sprintf("lsblk --json --nodeps --output size %s", diskIDPath) - res := framework.GetClients().D8Virtualization().SSHCommand(vmName, cmd, d8.SSHOptions{ - Namespace: vmNamespace, - Username: conf.TestData.SSHUser, - IdentityFile: conf.TestData.Sshkey, - }) - if res.Error() != nil { - return nil, errors.New(res.StdErr()) - } - err := json.Unmarshal(res.StdOutBytes(), &blockDevices) - if err != nil { - return nil, err - } - if len(blockDevices.BlockDevices) != 1 { - return nil, fmt.Errorf("`blockDevices` length should be 1") - } - blockDevice := &blockDevices.BlockDevices[0] - quantity = resource.MustParse(blockDevice.Size) - return &quantity, nil -} - -func GetDiskSize(vmNamespace, vmName, vdName, diskIDPath string, disk *DiskMetaData) { - GinkgoHelper() - sizeFromObject, err := GetSizeFromObject(vdName, vmNamespace) - Expect(err).NotTo(HaveOccurred(), "%v", err) - var sizeByLsblk *resource.Quantity - Eventually(func() error { - sizeByLsblk, err = GetSizeByLsblk(vmNamespace, vmName, diskIDPath) - if err != nil { - return fmt.Errorf("virtualMachine: %s\nstderr: %w", vmName, err) - } - return nil - }).WithTimeout(Timeout).WithPolling(Interval).Should(Succeed()) - Expect(sizeFromObject).NotTo(BeNil()) - Expect(sizeByLsblk).NotTo(BeNil()) - disk.SizeFromObject = sizeFromObject - disk.SizeByLsblk = sizeByLsblk -} - -func GetDiskIDPath(vdName string, vmi *virtv1.VirtualMachineInstance) string { - diskName := fmt.Sprintf("vd-%s", vdName) - for _, disk := range vmi.Spec.Domain.Devices.Disks { - if disk.Name == diskName { - return fmt.Sprintf("/dev/disk/by-id/%s_%s", DiskIDPrefix, disk.Serial) - } - } - return "" -} - -// Refactor this flow when `target` field will be fixed for `VirtualMachine.Status.BlockDeviceRefs` -func GetVirtualMachineDisks(vmNamespace, vmName string) (VirtualMachineDisks, error) { - GinkgoHelper() - var vmObject v1alpha2.VirtualMachine - disks := make(map[string]DiskMetaData, 0) - err := GetObject(v1alpha2.VirtualMachineResource, vmName, &vmObject, kc.GetOptions{ - Namespace: vmNamespace, - }) - if err != nil { - return disks, err - } - - intVirtVmi := &virtv1.VirtualMachineInstance{} - err = GetObject(kc.ResourceKubevirtVMI, vmName, intVirtVmi, kc.GetOptions{ - Namespace: vmNamespace, - }) - if err != nil { - return disks, err - } - - for _, device := range vmObject.Status.BlockDeviceRefs { - disk := DiskMetaData{} - if device.Kind != v1alpha2.DiskDevice { - continue - } - diskIDPath := GetDiskIDPath(device.Name, intVirtVmi) - GetDiskSize(vmNamespace, vmName, device.Name, diskIDPath, &disk) - disks[device.Name] = disk - } - return disks, nil -} diff --git a/test/e2e/vm/virtual_disk_resizing.go b/test/e2e/vm/virtual_disk_resizing.go new file mode 100644 index 0000000000..019e803309 --- /dev/null +++ b/test/e2e/vm/virtual_disk_resizing.go @@ -0,0 +1,243 @@ +/* +Copyright 2026 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 vm + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/object" + "github.com/deckhouse/virtualization/test/e2e/internal/precheck" + "github.com/deckhouse/virtualization/test/e2e/internal/util" +) + +var _ = Describe("VirtualDiskResizing", Label(precheck.NoPrecheck), func() { + var ( + f *framework.Framework + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + f = framework.NewFramework("virtual-disk-resizing") + f.Before() + DeferCleanup(f.After) + }) + + It("resizes virtual disks", func() { + By("Environment preparation") + vdRoot := object.NewVDFromCVI("vd-root", f.Namespace().Name, object.PrecreatedCVIUbuntu, vd.WithSize(ptr.To(resource.MustParse("4Gi")))) + vdBlank := object.NewBlankVD("vd-blank", f.Namespace().Name, nil, ptr.To(resource.MustParse("100Mi"))) + vdAttach := object.NewBlankVD("vd-attach", f.Namespace().Name, nil, ptr.To(resource.MustParse("100Mi"))) + + vm := object.NewMinimalVM("vm-", f.Namespace().Name, + vmbuilder.WithName("vm"), + vmbuilder.WithBlockDeviceRefs( + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.VirtualDiskKind, + Name: vdRoot.Name, + }, + v1alpha2.BlockDeviceSpecRef{ + Kind: v1alpha2.VirtualDiskKind, + Name: vdBlank.Name, + }, + ), + ) + + vmbda := object.NewVMBDAFromDisk("blank-disk-attachment", vm.Name, vdAttach) + + err := f.CreateWithDeferredDeletion(ctx, vdRoot, vdBlank, vdAttach, vm, vmbda) + Expect(err).NotTo(HaveOccurred()) + + util.UntilObjectPhase(ctx, string(v1alpha2.MachineRunning), framework.LongTimeout, vm) + util.UntilSSHReady(f, vm, framework.LongTimeout) + util.UntilObjectPhase(ctx, string(v1alpha2.BlockDeviceAttachmentPhaseAttached), framework.ShortTimeout, vmbda) + + vdRootLsblkSize := util.GetBlockDeviceLsblkSize(ctx, f, vm, v1alpha2.VirtualDiskKind, vdRoot.Name) + vdBlankLsblkSize := util.GetBlockDeviceLsblkSize(ctx, f, vm, v1alpha2.VirtualDiskKind, vdBlank.Name) + vdAttachLsblkSize := util.GetBlockDeviceLsblkSize(ctx, f, vm, v1alpha2.VirtualDiskKind, vdAttach.Name) + + By("Resize the disks") + ctxVDWatch, cancelVDWatch := context.WithCancel(ctx) + defer cancelVDWatch() + vdWatchErrCh := make(chan error, 1) + var vdWasResizing atomic.Bool + + go func() { + wasResizing, err := ensureVDWasResizing( + ctxVDWatch, + f.VirtClient().VirtualDisks(f.Namespace().Name), + []*v1alpha2.VirtualDisk{vdRoot, vdBlank, vdAttach}, + ) + vdWasResizing.Store(wasResizing) + vdWatchErrCh <- err + }() + + newVDRootSize, err := increaseDiskSize(ctx, f, vdRoot) + Expect(err).NotTo(HaveOccurred()) + newVDBlankSize, err := increaseDiskSize(ctx, f, vdBlank) + Expect(err).NotTo(HaveOccurred()) + newVDAttachSize, err := increaseDiskSize(ctx, f, vdAttach) + Expect(err).NotTo(HaveOccurred()) + + Eventually(vdWasResizing.Load).WithTimeout(framework.LongTimeout).WithPolling(time.Second).Should(BeTrue()) + + cancelVDWatch() + Expect(<-vdWatchErrCh).ShouldNot(HaveOccurred()) + + By("Verify that disks report the new size") + util.UntilObjectPhase(ctx, string(v1alpha2.DiskReady), framework.MiddleTimeout, vdRoot, vdBlank, vdAttach) + util.UntilObjectPhase(ctx, string(v1alpha2.MachineRunning), framework.ShortTimeout, vm) + util.UntilObjectPhase(ctx, string(v1alpha2.BlockDeviceAttachmentPhaseAttached), framework.ShortTimeout, vmbda) + + err = f.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vdRoot), vdRoot) + Expect(err).NotTo(HaveOccurred()) + err = f.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vdBlank), vdBlank) + Expect(err).NotTo(HaveOccurred()) + err = f.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vdAttach), vdAttach) + Expect(err).NotTo(HaveOccurred()) + + Expect(newVDRootSize.Cmp(resource.MustParse(vdRoot.Status.Capacity))).To(BeZero()) + Expect(newVDBlankSize.Cmp(resource.MustParse(vdBlank.Status.Capacity))).To(BeZero()) + Expect(newVDAttachSize.Cmp(resource.MustParse(vdAttach.Status.Capacity))).To(BeZero()) + + newVDRootLsblkSize := util.GetBlockDeviceLsblkSize(ctx, f, vm, v1alpha2.VirtualDiskKind, vdRoot.Name) + newVDBlankLsblkSize := util.GetBlockDeviceLsblkSize(ctx, f, vm, v1alpha2.VirtualDiskKind, vdBlank.Name) + newVDAttachLsblkSize := util.GetBlockDeviceLsblkSize(ctx, f, vm, v1alpha2.VirtualDiskKind, vdAttach.Name) + + Expect(newVDRootLsblkSize.Cmp(vdRootLsblkSize)).To(Equal(common.CmpGreater)) + Expect(newVDBlankLsblkSize.Cmp(vdBlankLsblkSize)).To(Equal(common.CmpGreater)) + Expect(newVDAttachLsblkSize.Cmp(vdAttachLsblkSize)).To(Equal(common.CmpGreater)) + + untilDisksArePresentAndAttachedInVMStatus(ctx, f, framework.ShortTimeout, vm, vdRoot, vdBlank, vdAttach) + }) +}) + +func increaseDiskSize(ctx context.Context, f *framework.Framework, vd *v1alpha2.VirtualDisk) (resource.Quantity, error) { + err := f.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vd), vd) + if err != nil { + return resource.Quantity{}, err + } + + if vd.Spec.PersistentVolumeClaim.Size == nil { + return resource.Quantity{}, fmt.Errorf("virtual disk %s/%s must have PVC size in spec", vd.Namespace, vd.Name) + } + size := *vd.Spec.PersistentVolumeClaim.Size + size.Add(resource.MustParse("100Mi")) + vd.Spec.PersistentVolumeClaim.Size = ptr.To(size) + + err = f.GenericClient().Update(ctx, vd) + if err != nil { + return resource.Quantity{}, err + } + + return size, nil +} + +// ensureVDWasResizing watches VDs and returns true when each tracked VD +// reaches the Resizing phase at least once before context cancellation. +func ensureVDWasResizing(ctx context.Context, w util.Watcher, vds []*v1alpha2.VirtualDisk) (bool, error) { + if len(vds) == 0 { + return true, nil + } + + tracked := make(map[string]struct{}, len(vds)) + seenResizing := make(map[string]struct{}, len(vds)) + for _, vd := range vds { + if vd == nil { + continue + } + tracked[vd.Name] = struct{}{} + if vd.Status.Phase == v1alpha2.DiskResizing { + seenResizing[vd.Name] = struct{}{} + } + } + + if len(tracked) == 0 || len(seenResizing) == len(tracked) { + return true, nil + } + + wi, err := w.Watch(ctx, metav1.ListOptions{}) + if err != nil { + return false, err + } + defer wi.Stop() + + for { + select { + case <-ctx.Done(): + return len(seenResizing) == len(tracked), nil + case event, ok := <-wi.ResultChan(): + if !ok { + if ctx.Err() != nil { + return len(seenResizing) == len(tracked), nil + } + return false, fmt.Errorf("watch channel closed unexpectedly while VDs were still being monitored") + } + + vd, ok := event.Object.(*v1alpha2.VirtualDisk) + if !ok { + continue + } + + if _, isTracked := tracked[vd.Name]; !isTracked { + continue + } + + if vd.Status.Phase == v1alpha2.DiskResizing { + seenResizing[vd.Name] = struct{}{} + if len(seenResizing) == len(tracked) { + return true, nil + } + } + } + } +} + +func untilDisksArePresentAndAttachedInVMStatus(ctx context.Context, f *framework.Framework, timeout time.Duration, vm *v1alpha2.VirtualMachine, vds ...*v1alpha2.VirtualDisk) { + GinkgoHelper() + + Eventually(func(g Gomega) { + err := f.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vm), vm) + g.Expect(err).NotTo(HaveOccurred()) + + statusActiveDisks := make(map[string]struct{}) + for _, bd := range vm.Status.BlockDeviceRefs { + if bd.Kind == v1alpha2.VirtualDiskKind && bd.Attached { + statusActiveDisks[bd.Name] = struct{}{} + } + } + + for _, vd := range vds { + g.Expect(statusActiveDisks).To(HaveKey(vd.Name)) + } + }).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed()) +}