Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
57f0d39
fix(e2e): mount usb block device in vm test
danilrwx Apr 28, 2026
b6754aa
fix(e2e): check usb detached before vm creation
danilrwx Apr 28, 2026
0f4d58e
fix(e2e): wait for usb block device
danilrwx Apr 28, 2026
1b255b2
fix(e2e): flush usb data before migration
danilrwx Apr 28, 2026
d75717a
fix(e2e): detect usb disk without transport
danilrwx Apr 28, 2026
2b13d48
fix(e2e): avoid fallback to non-usb disk
danilrwx Apr 28, 2026
939bd36
fix(e2e): rescan usb storage before mount
danilrwx Apr 28, 2026
ceb64c8
fix(e2e): increase usb block device wait timeout
danilrwx Apr 28, 2026
099ed8d
refactor(e2e): simplify usb mount detection
danilrwx Apr 28, 2026
5a377a0
fix(e2e): retry usb device mount
danilrwx Apr 28, 2026
1253006
fix(e2e): refresh usb device path after migration
danilrwx Apr 28, 2026
50840ba
refactor(e2e): retry usb mount with eventually
danilrwx Apr 28, 2026
0de88d4
fix(e2e): find usb block device by serial
danilrwx Apr 28, 2026
875ed7c
refactor(e2e): remove unused usb device path
danilrwx Apr 28, 2026
0f82fe3
refactor(e2e): simplify usb mount command
danilrwx Apr 28, 2026
2e7b97f
fix(e2e): improve usb mount diagnostics
danilrwx Apr 28, 2026
923c5a9
test(e2e): reduce usb mount retry timeout
danilrwx Apr 28, 2026
a72d1c6
fix(e2e): resolve usb block device via sysfs parent
danilrwx Apr 28, 2026
915ccb0
fix(e2e): match usb block device by serial
danilrwx Apr 28, 2026
b7ef4ed
fix(e2e): match usb disk by transport and removable
danilrwx Apr 28, 2026
0732456
refactor(e2e): simplify usb disk lookup
danilrwx Apr 28, 2026
a9969ba
fix(e2e): remount usb after migration
danilrwx Apr 28, 2026
dcd2836
fix(e2e): try vfat mount for usb disk
danilrwx Apr 28, 2026
5a2c284
refactor(e2e): extract usb vm test helpers
danilrwx Apr 28, 2026
66e86fa
fix(e2e): rescan scsi hosts before usb mount
danilrwx Apr 28, 2026
71a7617
test(e2e): increase usb mount retry timeout
danilrwx Apr 29, 2026
700b2bd
test(test): add guest command readiness helper
danilrwx Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions test/e2e/internal/util/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"regexp"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -184,6 +185,32 @@ func UntilSSHReady(f *framework.Framework, vm *v1alpha2.VirtualMachine, timeout
}).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed())
}

func UntilGuestCommandsReady(f *framework.Framework, vm *v1alpha2.VirtualMachine, commands []string, timeout time.Duration) {
GinkgoHelper()

cmd := fmt.Sprintf(`
missing=""
for command in %s; do
command -v "$command" >/dev/null 2>&1 || missing="$missing $command"
done
[ -z "$missing" ] || { echo "missing commands:$missing"; exit 1; }
`, shellArgs(commands))

Eventually(func() error {
_, err := f.SSHCommand(vm.Name, vm.Namespace, cmd, framework.WithSSHTimeout(5*time.Second))
return err
}).WithTimeout(timeout).WithPolling(time.Second).Should(Succeed())
}

func shellArgs(args []string) string {
quoted := make([]string, 0, len(args))
for _, arg := range args {
quoted = append(quoted, fmt.Sprintf("%q", arg))
}

return strings.Join(quoted, " ")
}

func UntilVMMigrationSucceeded(key client.ObjectKey, timeout time.Duration) {
GinkgoHelper()

Expand Down
184 changes: 123 additions & 61 deletions test/e2e/vm/usb.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,58 +62,39 @@ var _ = Describe("VirtualMachineUSB", func() {
}

t.GenerateEnvironmentResources()
err := f.CreateWithDeferredDeletion(context.Background(), t.VD, t.VM)
err := f.CreateWithDeferredDeletion(context.Background(), t.VD)
Expect(err).NotTo(HaveOccurred())

t.assignNodeUSB()

util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, t.VM)
util.UntilSSHReady(f, t.VM, framework.MiddleTimeout)
})

By("Verifying NodeUSBDevice is not attached before VM attachment", func() {
Eventually(func(g Gomega) {
nodeUSBDevice, err := t.Framework.VirtClient().NodeUSBDevices().Get(t.ctx, t.NodeUSBDevice.Name, metav1.GetOptions{})
g.Expect(err).NotTo(HaveOccurred())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice)).NotTo(BeNil())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice).Status).To(Equal(metav1.ConditionFalse))
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
t.waitForNodeUSBAttached(metav1.ConditionFalse)
})

By("Creating VM with USB device", func() {
err := f.CreateWithDeferredDeletion(context.Background(), t.VM)
Expect(err).NotTo(HaveOccurred())

util.UntilObjectPhase(string(v1alpha2.MachineRunning), framework.LongTimeout, t.VM)
util.UntilSSHReady(f, t.VM, framework.MiddleTimeout)
util.UntilGuestCommandsReady(f, t.VM, []string{"sudo", "tee", "udevadm"}, framework.LongTimeout)
})

By("Waiting for USB device to be attached and ready", func() {
Eventually(func() error {
vm, err := t.Framework.VirtClient().VirtualMachines(t.VM.Namespace).Get(t.ctx, t.VM.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())

for _, dev := range vm.Status.USBDevices {
if dev.Name == t.NodeUSBDevice.Name && dev.Attached && dev.Ready {
t.DevicePath = fmt.Sprintf("/dev/bus/usb/%d/%d", dev.Address.Bus, dev.Address.Port)
return nil
}
}

return fmt.Errorf("USB device %s not attached or not ready", t.NodeUSBDevice.Name)
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
t.waitForVMUSBReady("USB device %s not attached or not ready")
})

By("Verifying NodeUSBDevice is attached", func() {
Eventually(func(g Gomega) {
nodeUSBDevice, err := t.Framework.VirtClient().NodeUSBDevices().Get(t.ctx, t.NodeUSBDevice.Name, metav1.GetOptions{})
g.Expect(err).NotTo(HaveOccurred())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice)).NotTo(BeNil())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice).Status).To(Equal(metav1.ConditionTrue))
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
t.waitForNodeUSBAttached(metav1.ConditionTrue)
})

By("Mounting USB device", func() {
t.mountUSB()
})

By("Writing data to USB device", func() {
result, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, fmt.Sprintf("echo \"%s\" | sudo tee %s", t.testContent, t.testFile))

Expect(err).NotTo(HaveOccurred())
Expect(result).To(ContainSubstring(t.testContent))
t.writeUSBTestData()
})

By("Migrating VM", func() {
Expand All @@ -125,37 +106,19 @@ var _ = Describe("VirtualMachineUSB", func() {
})

By("Waiting for USB device to be ready after migration", func() {
Eventually(func() error {
vm, err := t.Framework.VirtClient().VirtualMachines(t.VM.Namespace).Get(t.ctx, t.VM.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())

for _, dev := range vm.Status.USBDevices {
if dev.Name == t.NodeUSBDevice.Name && dev.Attached && dev.Ready {
return nil
}
}

return fmt.Errorf("USB device %s not ready after migration", t.NodeUSBDevice.Name)
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
t.waitForVMUSBReady("USB device %s not ready after migration")
})

By("Verifying NodeUSBDevice is attached after migration", func() {
Eventually(func(g Gomega) {
nodeUSBDevice, err := t.Framework.VirtClient().NodeUSBDevices().Get(t.ctx, t.NodeUSBDevice.Name, metav1.GetOptions{})
g.Expect(err).NotTo(HaveOccurred())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice)).NotTo(BeNil())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice).Status).To(Equal(metav1.ConditionTrue))
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
t.waitForNodeUSBAttached(metav1.ConditionTrue)
})

By("Remounting USB device after migration", func() {
t.mountUSB()
})

By("Verifying data persists after migration", func() {
result, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, fmt.Sprintf("cat %s", t.testFile))
Expect(err).NotTo(HaveOccurred())
Expect(result).To(ContainSubstring(t.testContent))
t.verifyUSBTestData()
})
})
})
Expand All @@ -167,7 +130,6 @@ type VMUSBTest struct {
VM *v1alpha2.VirtualMachine
VD *v1alpha2.VirtualDisk
NodeUSBDevice *v1alpha2.NodeUSBDevice
DevicePath string

testFile string
testContent string
Expand Down Expand Up @@ -256,15 +218,115 @@ func (t *VMUSBTest) assignNodeUSB() {
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
}

func (t *VMUSBTest) waitForNodeUSBAttached(status metav1.ConditionStatus) {
Eventually(func(g Gomega) {
nodeUSBDevice, err := t.Framework.VirtClient().NodeUSBDevices().Get(t.ctx, t.NodeUSBDevice.Name, metav1.GetOptions{})
g.Expect(err).NotTo(HaveOccurred())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice)).NotTo(BeNil())
g.Expect(nodeUSBAttachedCondition(nodeUSBDevice).Status).To(Equal(status))
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
}

func (t *VMUSBTest) waitForVMUSBReady(message string) {
Eventually(func() error {
vm, err := t.Framework.VirtClient().VirtualMachines(t.VM.Namespace).Get(t.ctx, t.VM.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())

for _, dev := range vm.Status.USBDevices {
if dev.Name == t.NodeUSBDevice.Name && dev.Attached && dev.Ready {
return nil
}
}

return fmt.Errorf(message, t.NodeUSBDevice.Name)
}).WithTimeout(framework.MaxTimeout).WithPolling(time.Second).Should(Succeed())
}

func (t *VMUSBTest) writeUSBTestData() {
result, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, fmt.Sprintf("echo \"%s\" | sudo tee %s && sudo sync && sudo umount /mnt/usb", t.testContent, t.testFile))
Expect(err).NotTo(HaveOccurred())
Expect(result).To(ContainSubstring(t.testContent))
}

func (t *VMUSBTest) verifyUSBTestData() {
result, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, fmt.Sprintf("cat %s", t.testFile))
Expect(err).NotTo(HaveOccurred())
Expect(result).To(ContainSubstring(t.testContent))
}

func (t *VMUSBTest) mountUSB() {
serial := t.NodeUSBDevice.Status.Attributes.Serial
Expect(serial).NotTo(BeEmpty(), "USB device serial must be set")

mountCmd := fmt.Sprintf(`
sudo mkdir -p /mnt/usb || true && \
sudo mount %s /mnt/usb 2>/dev/null || sudo mount -o rw %s /mnt/usb || true && \
ls -la /mnt/usb || true
`, t.DevicePath, t.DevicePath)
usb_serial=%q
: > /tmp/usb-mount.err
for serial_file in /sys/bus/usb/devices/*/serial; do
if [ -f "$serial_file" ] && [ "$(cat "$serial_file")" = "$usb_serial" ]; then
usb_present=1
break
fi
done
[ -n "$usb_present" ] || { echo "USB device with serial $usb_serial not found" >/tmp/usb-mount.err; exit 1; }

sudo modprobe usb-storage
sudo modprobe uas || true

for host in /sys/class/scsi_host/host*; do
echo "- - -" | sudo tee "$host/scan" >/dev/null || true
done

sudo udevadm trigger
sudo udevadm settle

for dev in /dev/sd*; do
[ -b "$dev" ] || continue
if lsblk -dno TRAN,RM "$dev" 2>/dev/null | grep -Eq '^usb[[:space:]]+1$'; then
mount_device="$dev"
break
fi
done
[ -n "$mount_device" ] || {
echo "USB block device not found for serial $usb_serial" >>/tmp/usb-mount.err
lsblk -a -o NAME,PATH,TYPE,TRAN,RM,SERIAL,MODEL >>/tmp/usb-mount.err 2>&1 || true
exit 1
}

_, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, mountCmd)
Expect(err).NotTo(HaveOccurred())
sudo mkdir -p /mnt/usb
if sudo mountpoint -q /mnt/usb; then
sudo umount /mnt/usb || true
fi
sudo mount -t auto "$mount_device" /mnt/usb 2>>/tmp/usb-mount.err || \
sudo mount -t vfat -o rw "$mount_device" /mnt/usb 2>>/tmp/usb-mount.err || \
sudo mount -o rw "$mount_device" /mnt/usb 2>>/tmp/usb-mount.err
ls -la /mnt/usb
`, serial)

Eventually(func() error {
_, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, mountCmd, framework.WithSSHTimeout(framework.MiddleTimeout))
return err
}).WithTimeout(framework.MiddleTimeout).WithPolling(time.Second).Should(Succeed(), t.usbDiagnostics())
}

func (t *VMUSBTest) usbDiagnostics() string {
diagnosticsCmd := `
echo "mount error:" && cat /tmp/usb-mount.err 2>/dev/null || true
echo "mount:" && mount || true
echo "usb serials:" && for serial_file in /sys/bus/usb/devices/*/serial; do [ -f "$serial_file" ] && echo "$serial_file=$(cat "$serial_file")"; done || true
echo "usb sysfs:" && find /sys/bus/usb/devices -maxdepth 3 -print || true
echo "lsblk:" && lsblk -a -o NAME,PATH,TYPE,TRAN,RM,SERIAL,MODEL || true
echo "disks:" && for dev in /dev/sd*; do [ -b "$dev" ] && echo "== $dev ==" && lsblk -dno NAME,PATH,TRAN,RM,SERIAL,MODEL "$dev"; done || true
echo "lsusb:" && lsusb || true
echo "fstype:" && blkid /dev/sd* || true
echo "dmesg:" && sudo dmesg | tail -n 100 || true
`

result, err := t.Framework.SSHCommand(t.VM.Name, t.VM.Namespace, diagnosticsCmd, framework.WithSSHTimeout(framework.MiddleTimeout))
if err != nil {
return fmt.Sprintf("failed to collect USB diagnostics: %v", err)
}

return result
}

func nodeUSBAttachedCondition(nodeUSBDevice *v1alpha2.NodeUSBDevice) *metav1.Condition {
Expand Down
Loading