tests: Cover direct IO data disks on 4k sector FS

Add a parameterized helper that creates a 1 GiB ext4 loop filesystem
with 4096 byte sectors, populates it with a small data disk in the
requested format, attaches that disk with direct=on, and runs a 4096
byte aligned dd round trip with oflag=direct and iflag=direct
followed by cmp.

Wrappers exercise raw, qcow2, fixed VHD, and vhdx. The qcow2 and vhdx
wrappers expect the guest to see the on disk LBS of 512. The others
expect the host LBS of 4096.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2026-06-19 22:29:29 +02:00
committed by Rob Bradford
parent 3f20fd0759
commit dd2f18e73e

View File

@@ -1589,6 +1589,149 @@ mod common_parallel {
handle_child_output(r, &output);
}
// Direct=on data disk on a 4k sector loop FS, aligned O_DIRECT round trip.
fn _test_virtio_block_direct_io_data_disk_4k(image_type: ImageType) {
let (qemu_fmt, qemu_extra): (&str, &[&str]) = match image_type {
ImageType::Raw => ("raw", &[]),
ImageType::Qcow2 => ("qcow2", &[]),
ImageType::FixedVhd => ("vpc", &["-o", "subformat=fixed"]),
ImageType::Vhdx => ("vhdx", &[]),
ImageType::Unknown => panic!("unsupported image_type {image_type}"),
};
let image_type_str = image_type.to_string();
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
// The loop backing file needs a real O_DIRECT capable FS, not tmpfs.
let mut workloads_path = dirs::home_dir().unwrap();
workloads_path.push("workloads");
let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap();
let fs_img_path = img_dir.as_path().join("fs_4ksec.img");
assert!(
exec_host_command_output(&format!("truncate -s 1G {}", fs_img_path.to_str().unwrap()))
.status
.success(),
"truncate failed"
);
let loop_dev_path = create_loop_device(fs_img_path.to_str().unwrap(), 4096, 5);
assert!(
exec_host_command_output(&format!("mkfs.ext4 -q {loop_dev_path}"))
.status
.success(),
"mkfs.ext4 failed"
);
let mnt_dir = img_dir.as_path().join("mnt");
fs::create_dir_all(&mnt_dir).unwrap();
assert!(
exec_host_command_output(&format!(
"mount {} {}",
loop_dev_path,
mnt_dir.to_str().unwrap()
))
.status
.success(),
"mount failed"
);
let test_disk = mnt_dir.join(format!("data.{image_type_str}"));
let mut create_args: Vec<&str> = vec!["create", "-f", qemu_fmt];
create_args.extend_from_slice(qemu_extra);
let res = run_qemu_img(&test_disk, &create_args, Some(&["64M"]));
assert!(res.status.success(), "qemu-img create failed: {res:?}");
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=4"])
.args(["--memory", "size=512M"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
format!(
"path={}",
guest.disk_config.disk(DiskType::OperatingSystem).unwrap()
)
.as_str(),
format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
)
.as_str(),
format!(
"path={},direct=on,image_type={image_type_str}",
test_disk.to_str().unwrap()
)
.as_str(),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
// qcow2 and vhdx report 512 LBS from the on disk format. Raw and
// fixed VHD pass through the host 4096.
let expected_log_sec: u32 = match image_type {
ImageType::Qcow2 | ImageType::Vhdx => 512,
_ => 4096,
};
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot().unwrap();
let log_sec: u32 = guest
.ssh_command("lsblk -t | grep vdc | awk '{print $6}'")
.unwrap()
.trim()
.parse()
.unwrap_or_default();
assert_eq!(log_sec, expected_log_sec, "unexpected logical sector size");
guest
.ssh_command(
"sudo dd if=/dev/urandom of=/tmp/pattern bs=4096 count=8 && \
sudo dd if=/tmp/pattern of=/dev/vdc bs=4096 count=8 seek=1 \
oflag=direct conv=fsync && \
sudo dd if=/dev/vdc of=/tmp/readback bs=4096 count=8 skip=1 \
iflag=direct && \
cmp /tmp/pattern /tmp/readback",
)
.expect("aligned 4k direct IO round trip failed");
});
kill_child(&mut child);
let output = child.wait_with_output().unwrap();
let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap()));
let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}"));
handle_child_output(r, &output);
}
#[test]
fn test_virtio_block_direct_io_data_disk_4k_raw() {
_test_virtio_block_direct_io_data_disk_4k(ImageType::Raw);
}
#[test]
fn test_virtio_block_direct_io_data_disk_4k_qcow2() {
_test_virtio_block_direct_io_data_disk_4k(ImageType::Qcow2);
}
#[test]
fn test_virtio_block_direct_io_data_disk_4k_vhd() {
_test_virtio_block_direct_io_data_disk_4k(ImageType::FixedVhd);
}
#[test]
fn test_virtio_block_direct_io_data_disk_4k_vhdx() {
_test_virtio_block_direct_io_data_disk_4k(ImageType::Vhdx);
}
#[test]
fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());