From 7994c57572dbbd81a241d12594154253ef82ce63 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 25 May 2026 12:21:38 +0200 Subject: [PATCH 1/5] test(test): make lsblk size check retryable Signed-off-by: Daniil Antoshin --- test/e2e/internal/util/block_device.go | 50 +++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/test/e2e/internal/util/block_device.go b/test/e2e/internal/util/block_device.go index ddd2c14d0b..b3ee7ee6be 100644 --- a/test/e2e/internal/util/block_device.go +++ b/test/e2e/internal/util/block_device.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "strings" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -109,25 +110,50 @@ func GetBlockDeviceHash(ctx context.Context, f *framework.Framework, vm *v1alpha func GetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm *v1alpha2.VirtualMachine, bdKind v1alpha2.BlockDeviceKind, bdName string) resource.Quantity { GinkgoHelper() + var size resource.Quantity + Eventually(func(g Gomega) { + quantity, err := tryGetBlockDeviceLsblkSize(ctx, f, vm, bdKind, bdName) + g.Expect(err).NotTo(HaveOccurred(), "failed to get lsblk size for block device %s/%s", bdKind, bdName) + size = quantity + }).WithTimeout(framework.MiddleTimeout).WithPolling(time.Second).Should(Succeed()) + + return size +} + +func tryGetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm *v1alpha2.VirtualMachine, bdKind v1alpha2.BlockDeviceKind, bdName string) (resource.Quantity, error) { serial, ok := GetBlockDeviceSerialNumber(ctx, vm, bdKind, bdName) - Expect(ok).To(BeTrue(), fmt.Sprintf("failed to get block device %s/%s serial number", bdKind, bdName)) + if !ok { + return resource.Quantity{}, fmt.Errorf("failed to get block device %s/%s serial number", bdKind, bdName) + } - devicePath, err := GetBlockDeviceBySerial(f, vm, serial) - Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("failed to get device %s/%s by serial", bdKind, bdName)) + devicePath, err := GetBlockDeviceBySerial(f, vm, serial, framework.WithSSHTimeout(10*time.Second)) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to get device %s/%s by serial: %w", bdKind, bdName, err) + } - cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --json -o SIZE %s", devicePath)) - Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("failed to get lsblk size for block device %s/%s", bdKind, bdName)) + cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --json -o SIZE %s", devicePath), framework.WithSSHTimeout(10*time.Second)) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to get lsblk size for block device %s/%s: %w", bdKind, bdName, err) + } 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") + if err = json.Unmarshal([]byte(cmdOut), &disks); err != nil { + return resource.Quantity{}, fmt.Errorf("failed to parse lsblk output: %w", err) + } + if len(disks.BlockDevices) == 0 { + return resource.Quantity{}, fmt.Errorf("lsblk output does not contain block devices") + } + + quantity, err := resource.ParseQuantity(strings.TrimSpace(disks.BlockDevices[0].Size)) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to parse lsblk size: %w", err) + } - return resource.MustParse(strings.TrimSpace(disks.BlockDevices[0].Size)) + return quantity, nil } -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)) +func GetBlockDeviceBySerial(f *framework.Framework, vm *v1alpha2.VirtualMachine, serial string, options ...framework.SSHCommandOption) (string, error) { + cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --nodeps --noheadings -o PATH,SERIAL | awk \"\\$2 == \\\"%s\\\" {print \\$1, \\$2; exit}\"", serial), options...) if err != nil { return "", err } @@ -137,7 +163,7 @@ func GetBlockDeviceBySerial(f *framework.Framework, vm *v1alpha2.VirtualMachine, return "", errors.New("shell out is empty") } - columns := strings.Split(strings.TrimSpace(cmdLines[0]), " ") + columns := strings.Fields(cmdLines[0]) if len(columns) != 2 { return "", errors.New("shell out columns mismatch") } From 7ed237557688c4fa84ddf1e69b64e7ad8a280d32 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 25 May 2026 12:33:05 +0200 Subject: [PATCH 2/5] test(test): ignore log stream shutdown errors Signed-off-by: Daniil Antoshin --- test/e2e/controller/err_checker.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/e2e/controller/err_checker.go b/test/e2e/controller/err_checker.go index 03708168bf..8a215a40bd 100644 --- a/test/e2e/controller/err_checker.go +++ b/test/e2e/controller/err_checker.go @@ -80,10 +80,9 @@ func (l *LogChecker) Start() error { l.mu.Lock() defer l.mu.Unlock() if err != nil && !errors.Is(err, context.Canceled) { - // TODO: Find an alternative way to store Virtualization Controller errors without streaming. - // `http2.GoAwayError` likely appears when the context is canceled and readers are closed. - // It should not cause tests to fail. - if strings.Contains(err.Error(), "GOAWAY") { + if l.ctx.Err() != nil { + ginkgo.GinkgoWriter.Printf("Warning! %v\n", err) + } else if strings.Contains(err.Error(), "GOAWAY") { ginkgo.GinkgoWriter.Printf("Warning! %v\n", err) } else { l.resultErr = errors.Join(l.resultErr, err) @@ -97,10 +96,10 @@ func (l *LogChecker) Start() error { func (l *LogChecker) Stop() error { l.cancel() - l.wg.Wait() for _, c := range l.closers { _ = c.Close() } + l.wg.Wait() if l.resultErr != nil { return l.resultErr From 65f11cea325c2cd89ffd6d91b4eddcf80071f2a4 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 25 May 2026 13:16:04 +0200 Subject: [PATCH 3/5] test(test): satisfy log checker lint Signed-off-by: Daniil Antoshin --- test/e2e/controller/err_checker.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/e2e/controller/err_checker.go b/test/e2e/controller/err_checker.go index 8a215a40bd..92bf0bf081 100644 --- a/test/e2e/controller/err_checker.go +++ b/test/e2e/controller/err_checker.go @@ -80,11 +80,12 @@ func (l *LogChecker) Start() error { l.mu.Lock() defer l.mu.Unlock() if err != nil && !errors.Is(err, context.Canceled) { - if l.ctx.Err() != nil { + switch { + case l.ctx.Err() != nil: ginkgo.GinkgoWriter.Printf("Warning! %v\n", err) - } else if strings.Contains(err.Error(), "GOAWAY") { + case strings.Contains(err.Error(), "GOAWAY"): ginkgo.GinkgoWriter.Printf("Warning! %v\n", err) - } else { + default: l.resultErr = errors.Join(l.resultErr, err) } } From 5ba838e0131fa1e9da19478031f4f6808c4625c0 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 25 May 2026 18:29:09 +0200 Subject: [PATCH 4/5] test(test): reduce lsblk probes for disk size Signed-off-by: Daniil Antoshin --- test/e2e/internal/util/block_device.go | 38 ++++++++++++++------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/test/e2e/internal/util/block_device.go b/test/e2e/internal/util/block_device.go index b3ee7ee6be..fbcb862cf5 100644 --- a/test/e2e/internal/util/block_device.go +++ b/test/e2e/internal/util/block_device.go @@ -115,7 +115,7 @@ func GetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm *v1 quantity, err := tryGetBlockDeviceLsblkSize(ctx, f, vm, bdKind, bdName) g.Expect(err).NotTo(HaveOccurred(), "failed to get lsblk size for block device %s/%s", bdKind, bdName) size = quantity - }).WithTimeout(framework.MiddleTimeout).WithPolling(time.Second).Should(Succeed()) + }).WithTimeout(framework.LongTimeout).WithPolling(time.Second).Should(Succeed()) return size } @@ -126,30 +126,30 @@ func tryGetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm return resource.Quantity{}, fmt.Errorf("failed to get block device %s/%s serial number", bdKind, bdName) } - devicePath, err := GetBlockDeviceBySerial(f, vm, serial, framework.WithSSHTimeout(10*time.Second)) + cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, "sudo lsblk --nodeps --json -o PATH,SERIAL,SIZE") if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to get device %s/%s by serial: %w", bdKind, bdName, err) - } - - cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --json -o SIZE %s", devicePath), framework.WithSSHTimeout(10*time.Second)) - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to get lsblk size for block device %s/%s: %w", bdKind, bdName, err) + return resource.Quantity{}, fmt.Errorf("failed to get lsblk output for block device %s/%s: %w", bdKind, bdName, err) } var disks Disks if err = json.Unmarshal([]byte(cmdOut), &disks); err != nil { return resource.Quantity{}, fmt.Errorf("failed to parse lsblk output: %w", err) } - if len(disks.BlockDevices) == 0 { - return resource.Quantity{}, fmt.Errorf("lsblk output does not contain block devices") - } - quantity, err := resource.ParseQuantity(strings.TrimSpace(disks.BlockDevices[0].Size)) - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to parse lsblk size: %w", err) + for _, blockDevice := range disks.BlockDevices { + if blockDevice.Serial != serial { + continue + } + + quantity, err := resource.ParseQuantity(strings.TrimSpace(blockDevice.Size)) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to parse lsblk size: %w", err) + } + + return quantity, nil } - return quantity, nil + return resource.Quantity{}, fmt.Errorf("lsblk output does not contain block device with serial %s", serial) } func GetBlockDeviceBySerial(f *framework.Framework, vm *v1alpha2.VirtualMachine, serial string, options ...framework.SSHCommandOption) (string, error) { @@ -269,7 +269,9 @@ type Disks struct { // BlockDevice represents a single block device in the lsblk JSON output. type BlockDevice struct { - Name string `json:"name"` - Size string `json:"size"` - Type string `json:"type"` + Name string `json:"name"` + Path string `json:"path"` + Serial string `json:"serial"` + Size string `json:"size"` + Type string `json:"type"` } From cd943b1400adb35c366e6aecd1edc375f664ffc6 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Mon, 25 May 2026 18:53:10 +0200 Subject: [PATCH 5/5] test(test): increase hotplug vm cpu fraction Signed-off-by: Daniil Antoshin --- test/e2e/controller/err_checker.go | 12 +++--- test/e2e/internal/util/block_device.go | 60 +++++++------------------- test/e2e/vm/block_device_hotplug.go | 2 +- 3 files changed, 23 insertions(+), 51 deletions(-) diff --git a/test/e2e/controller/err_checker.go b/test/e2e/controller/err_checker.go index 92bf0bf081..03708168bf 100644 --- a/test/e2e/controller/err_checker.go +++ b/test/e2e/controller/err_checker.go @@ -80,12 +80,12 @@ func (l *LogChecker) Start() error { l.mu.Lock() defer l.mu.Unlock() if err != nil && !errors.Is(err, context.Canceled) { - switch { - case l.ctx.Err() != nil: + // TODO: Find an alternative way to store Virtualization Controller errors without streaming. + // `http2.GoAwayError` likely appears when the context is canceled and readers are closed. + // It should not cause tests to fail. + if strings.Contains(err.Error(), "GOAWAY") { ginkgo.GinkgoWriter.Printf("Warning! %v\n", err) - case strings.Contains(err.Error(), "GOAWAY"): - ginkgo.GinkgoWriter.Printf("Warning! %v\n", err) - default: + } else { l.resultErr = errors.Join(l.resultErr, err) } } @@ -97,10 +97,10 @@ func (l *LogChecker) Start() error { func (l *LogChecker) Stop() error { l.cancel() + l.wg.Wait() for _, c := range l.closers { _ = c.Close() } - l.wg.Wait() if l.resultErr != nil { return l.resultErr diff --git a/test/e2e/internal/util/block_device.go b/test/e2e/internal/util/block_device.go index fbcb862cf5..ddd2c14d0b 100644 --- a/test/e2e/internal/util/block_device.go +++ b/test/e2e/internal/util/block_device.go @@ -22,7 +22,6 @@ import ( "errors" "fmt" "strings" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -110,50 +109,25 @@ func GetBlockDeviceHash(ctx context.Context, f *framework.Framework, vm *v1alpha func GetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm *v1alpha2.VirtualMachine, bdKind v1alpha2.BlockDeviceKind, bdName string) resource.Quantity { GinkgoHelper() - var size resource.Quantity - Eventually(func(g Gomega) { - quantity, err := tryGetBlockDeviceLsblkSize(ctx, f, vm, bdKind, bdName) - g.Expect(err).NotTo(HaveOccurred(), "failed to get lsblk size for block device %s/%s", bdKind, bdName) - size = quantity - }).WithTimeout(framework.LongTimeout).WithPolling(time.Second).Should(Succeed()) - - return size -} - -func tryGetBlockDeviceLsblkSize(ctx context.Context, f *framework.Framework, vm *v1alpha2.VirtualMachine, bdKind v1alpha2.BlockDeviceKind, bdName string) (resource.Quantity, error) { serial, ok := GetBlockDeviceSerialNumber(ctx, vm, bdKind, bdName) - if !ok { - return resource.Quantity{}, fmt.Errorf("failed to get block device %s/%s serial number", bdKind, bdName) - } - - cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, "sudo lsblk --nodeps --json -o PATH,SERIAL,SIZE") - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to get lsblk output for block device %s/%s: %w", bdKind, bdName, err) - } - - var disks Disks - if err = json.Unmarshal([]byte(cmdOut), &disks); err != nil { - return resource.Quantity{}, fmt.Errorf("failed to parse lsblk output: %w", err) - } + Expect(ok).To(BeTrue(), fmt.Sprintf("failed to get block device %s/%s serial number", bdKind, bdName)) - for _, blockDevice := range disks.BlockDevices { - if blockDevice.Serial != serial { - continue - } + devicePath, err := GetBlockDeviceBySerial(f, vm, serial) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("failed to get device %s/%s by serial", bdKind, bdName)) - quantity, err := resource.ParseQuantity(strings.TrimSpace(blockDevice.Size)) - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to parse lsblk size: %w", err) - } + cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --json -o SIZE %s", devicePath)) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("failed to get lsblk size for block device %s/%s", bdKind, bdName)) - return quantity, nil - } + 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.Quantity{}, fmt.Errorf("lsblk output does not contain block device with serial %s", serial) + return resource.MustParse(strings.TrimSpace(disks.BlockDevices[0].Size)) } -func GetBlockDeviceBySerial(f *framework.Framework, vm *v1alpha2.VirtualMachine, serial string, options ...framework.SSHCommandOption) (string, error) { - cmdOut, err := f.SSHCommand(vm.Name, vm.Namespace, fmt.Sprintf("sudo lsblk --nodeps --noheadings -o PATH,SERIAL | awk \"\\$2 == \\\"%s\\\" {print \\$1, \\$2; exit}\"", serial), options...) +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 { return "", err } @@ -163,7 +137,7 @@ func GetBlockDeviceBySerial(f *framework.Framework, vm *v1alpha2.VirtualMachine, return "", errors.New("shell out is empty") } - columns := strings.Fields(cmdLines[0]) + columns := strings.Split(strings.TrimSpace(cmdLines[0]), " ") if len(columns) != 2 { return "", errors.New("shell out columns mismatch") } @@ -269,9 +243,7 @@ type Disks struct { // BlockDevice represents a single block device in the lsblk JSON output. type BlockDevice struct { - Name string `json:"name"` - Path string `json:"path"` - Serial string `json:"serial"` - Size string `json:"size"` - Type string `json:"type"` + Name string `json:"name"` + Size string `json:"size"` + Type string `json:"type"` } diff --git a/test/e2e/vm/block_device_hotplug.go b/test/e2e/vm/block_device_hotplug.go index 1c2c268e80..e7562489d2 100644 --- a/test/e2e/vm/block_device_hotplug.go +++ b/test/e2e/vm/block_device_hotplug.go @@ -182,7 +182,7 @@ func setupVM(f *framework.Framework, withBlank bool) ( vm = vmbuilder.New( vmbuilder.WithName("vm"), vmbuilder.WithNamespace(f.Namespace().Name), - vmbuilder.WithCPU(1, ptr.To("5%")), + vmbuilder.WithCPU(1, ptr.To("100%")), vmbuilder.WithMemory(resource.MustParse("256Mi")), vmbuilder.WithLiveMigrationPolicy(v1alpha2.AlwaysSafeMigrationPolicy), vmbuilder.WithVirtualMachineClass(object.DefaultVMClass),