mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
block: Add integration tests
Adds integration tests for vmdk to cover i/o scenarios for single extent & multi-extent span read writes. All tests run disk consistency check in the end. The tests also extend direct_io for VMDK backend. Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
committed by
Wei Liu
parent
82b65c3352
commit
d69fb67f5b
@@ -2170,6 +2170,303 @@ pub fn _test_virtio_block_dynamic_vhdx_expand(guest: &Guest) {
|
|||||||
disk_check_consistency(vhdx_path, None);
|
disk_check_consistency(vhdx_path, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn _test_virtio_block_vmdk(guest: &Guest, subformat: &str) {
|
||||||
|
let raw_os_disk = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
|
||||||
|
let vmdk_path = guest.tmp_dir.as_path().join("osdisk.vmdk");
|
||||||
|
let vmdk_path_str = vmdk_path.to_str().unwrap();
|
||||||
|
|
||||||
|
// Convert the prepared RAW OS disk into a flat VMDK. qemu-img writes the
|
||||||
|
// extent file(s) next to the descriptor automatically.
|
||||||
|
let convert = Command::new("qemu-img")
|
||||||
|
.arg("convert")
|
||||||
|
.args(["-f", "raw"])
|
||||||
|
.args(["-O", "vmdk"])
|
||||||
|
.args(["-o", &format!("subformat={subformat}")])
|
||||||
|
.arg(&raw_os_disk)
|
||||||
|
.arg(vmdk_path_str)
|
||||||
|
.output()
|
||||||
|
.expect("Expect generating flat VMDK image from RAW image");
|
||||||
|
assert!(
|
||||||
|
convert.status.success(),
|
||||||
|
"qemu-img convert to VMDK (subformat={subformat}) failed: {}",
|
||||||
|
String::from_utf8_lossy(&convert.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cloud_child = GuestCommand::new(guest)
|
||||||
|
.default_cpus()
|
||||||
|
.default_memory()
|
||||||
|
.default_kernel_cmdline()
|
||||||
|
.args([
|
||||||
|
"--disk",
|
||||||
|
// Force the flat VMDK backend explicitly instead of relying on
|
||||||
|
// image-format auto-detection.
|
||||||
|
format!("path={vmdk_path_str},image_type=vmdk").as_str(),
|
||||||
|
format!(
|
||||||
|
"path={}",
|
||||||
|
guest.disk_config.disk(DiskType::CloudInit).unwrap()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
])
|
||||||
|
.default_net()
|
||||||
|
.capture_output()
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let r = panic::catch_unwind(|| {
|
||||||
|
guest.wait_vm_boot().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1);
|
||||||
|
assert!(guest.get_total_memory().unwrap_or_default() > 480_000);
|
||||||
|
|
||||||
|
// Exercise the flat VMDK write + read paths through the guest root
|
||||||
|
// filesystem: write a marker file, flush, and read it back.
|
||||||
|
guest
|
||||||
|
.ssh_command(
|
||||||
|
"echo cloud-hypervisor-vmdk | sudo tee /root/vmdk_marker > /dev/null && sync",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
guest
|
||||||
|
.ssh_command("sudo cat /root/vmdk_marker")
|
||||||
|
.unwrap()
|
||||||
|
.trim(),
|
||||||
|
"cloud-hypervisor-vmdk"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
kill_child(&mut cloud_child);
|
||||||
|
let output = cloud_child.wait_with_output().unwrap();
|
||||||
|
|
||||||
|
handle_child_output(r, &output);
|
||||||
|
|
||||||
|
disk_check_consistency(vmdk_path_str, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn _test_virtio_block_vmdk_enospc(guest: &Guest, subformat: &str) {
|
||||||
|
const DISK_SIZE_MIB: u64 = 16;
|
||||||
|
|
||||||
|
let vmdk_path = guest.tmp_dir.as_path().join("data.vmdk");
|
||||||
|
let vmdk_path_str = vmdk_path.to_str().unwrap();
|
||||||
|
|
||||||
|
let create = Command::new("qemu-img")
|
||||||
|
.arg("create")
|
||||||
|
.args(["-f", "vmdk"])
|
||||||
|
.args(["-o", &format!("subformat={subformat}")])
|
||||||
|
.arg(vmdk_path_str)
|
||||||
|
.arg(format!("{DISK_SIZE_MIB}M"))
|
||||||
|
.output()
|
||||||
|
.expect("Expect creating flat VMDK data disk");
|
||||||
|
assert!(
|
||||||
|
create.status.success(),
|
||||||
|
"qemu-img create VMDK (subformat={subformat}) failed: {}",
|
||||||
|
String::from_utf8_lossy(&create.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cloud_child = GuestCommand::new(guest)
|
||||||
|
.default_cpus()
|
||||||
|
.default_memory()
|
||||||
|
.default_kernel_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(),
|
||||||
|
// Force the flat VMDK backend explicitly.
|
||||||
|
format!("path={vmdk_path_str},image_type=vmdk").as_str(),
|
||||||
|
])
|
||||||
|
.default_net()
|
||||||
|
.capture_output()
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let r = panic::catch_unwind(|| {
|
||||||
|
guest.wait_vm_boot().unwrap();
|
||||||
|
|
||||||
|
// The flat VMDK data disk shows up as vdc.
|
||||||
|
assert_eq!(
|
||||||
|
guest
|
||||||
|
.ssh_command("lsblk | grep -c vdc")
|
||||||
|
.unwrap()
|
||||||
|
.trim()
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1) In-bounds direct-IO round trip: write a random pattern near the
|
||||||
|
// start of the device and read it back to confirm the VMDK I/O
|
||||||
|
// path is correct.
|
||||||
|
guest
|
||||||
|
.ssh_command(
|
||||||
|
"sudo dd if=/dev/urandom of=/tmp/pattern bs=1M count=4 && \
|
||||||
|
sudo dd if=/tmp/pattern of=/dev/vdc bs=1M count=4 seek=0 \
|
||||||
|
oflag=direct conv=fsync && \
|
||||||
|
sudo dd if=/dev/vdc of=/tmp/readback bs=1M count=4 skip=0 \
|
||||||
|
iflag=direct && \
|
||||||
|
cmp /tmp/pattern /tmp/readback",
|
||||||
|
)
|
||||||
|
.expect("in-bounds flat VMDK direct IO round trip failed");
|
||||||
|
|
||||||
|
// 2) Writing past the fixed virtual capacity must fail with ENOSPC:
|
||||||
|
// a flat VMDK is pre-allocated and cannot grow. dd exits non-zero
|
||||||
|
// once the device is full, so `|| true` keeps ssh_command happy
|
||||||
|
// while we assert on the reported error.
|
||||||
|
let enospc = guest
|
||||||
|
.ssh_command(&format!(
|
||||||
|
"sudo dd if=/dev/zero of=/dev/vdc bs=1M count={} oflag=direct 2>&1 || true",
|
||||||
|
DISK_SIZE_MIB + 8
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
enospc.contains("No space left on device"),
|
||||||
|
"expected ENOSPC writing past end of pre-allocated flat VMDK, got: {enospc}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
kill_child(&mut cloud_child);
|
||||||
|
let output = cloud_child.wait_with_output().unwrap();
|
||||||
|
|
||||||
|
handle_child_output(r, &output);
|
||||||
|
|
||||||
|
disk_check_consistency(vmdk_path_str, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn _test_virtio_block_vmdk_extent_spanning(guest: &Guest, direct: bool) {
|
||||||
|
// 3 GiB virtual size => a 2 GiB first extent + a 1 GiB second extent.
|
||||||
|
// `direct=on` opens the extents with `O_DIRECT`, which tmpfs rejects with
|
||||||
|
// EINVAL. guest.tmp_dir is backed by tmpfs, so for the direct
|
||||||
|
// case place the VMDK on ~/workloads instead (same rationale as
|
||||||
|
// test_virtio_block_direct_and_firmware).
|
||||||
|
let workloads_tmp_dir;
|
||||||
|
let base_dir = if direct {
|
||||||
|
let mut workloads_path = dirs::home_dir().unwrap();
|
||||||
|
workloads_path.push("workloads");
|
||||||
|
workloads_tmp_dir = TempDir::new_in(workloads_path.as_path()).unwrap();
|
||||||
|
workloads_tmp_dir.as_path()
|
||||||
|
} else {
|
||||||
|
guest.tmp_dir.as_path()
|
||||||
|
};
|
||||||
|
let vmdk_path = base_dir.join("spanning.vmdk");
|
||||||
|
let vmdk_path_str = vmdk_path.to_str().unwrap();
|
||||||
|
|
||||||
|
let create = Command::new("qemu-img")
|
||||||
|
.arg("create")
|
||||||
|
.args(["-f", "vmdk"])
|
||||||
|
.args(["-o", "subformat=twoGbMaxExtentFlat"])
|
||||||
|
.arg(vmdk_path_str)
|
||||||
|
.arg("3G")
|
||||||
|
.output()
|
||||||
|
.expect("Expect creating twoGbMaxExtentFlat VMDK data disk");
|
||||||
|
assert!(
|
||||||
|
create.status.success(),
|
||||||
|
"qemu-img create twoGbMaxExtentFlat VMDK failed: {}",
|
||||||
|
String::from_utf8_lossy(&create.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let descriptor = fs::read_to_string(&vmdk_path).expect("read VMDK descriptor");
|
||||||
|
let extent_lines: Vec<&str> = descriptor
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.starts_with("RW ") && l.contains("FLAT"))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
extent_lines.len() >= 2,
|
||||||
|
"twoGbMaxExtentFlat should produce >= 2 extents, descriptor was:\n{descriptor}"
|
||||||
|
);
|
||||||
|
let first_extent_sectors: u64 = extent_lines[0]
|
||||||
|
.split_whitespace()
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.expect("parse first extent sector count from descriptor");
|
||||||
|
let boundary_bytes = first_extent_sectors * 512;
|
||||||
|
|
||||||
|
// Straddle the boundary with a 2 MiB, 4 KiB-aligned direct-IO window
|
||||||
|
// (~1 MiB in each extent). Round the start down to the O_DIRECT alignment
|
||||||
|
// without assuming the extent size itself is 4 KiB aligned.
|
||||||
|
const BS: u64 = 4096;
|
||||||
|
const HALF: u64 = 1024 * 1024;
|
||||||
|
let raw_start = boundary_bytes - HALF;
|
||||||
|
let start = raw_start - (raw_start % BS);
|
||||||
|
let seek = start / BS;
|
||||||
|
let count = (2 * HALF) / BS;
|
||||||
|
assert!(
|
||||||
|
start < boundary_bytes && boundary_bytes < start + count * BS,
|
||||||
|
"write window [{start}, {}) does not straddle extent boundary {boundary_bytes}",
|
||||||
|
start + count * BS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force the flat VMDK backend explicitly, and honor the requested cache
|
||||||
|
// mode. `direct=on` opens the extents with `O_DIRECT` on the host.
|
||||||
|
let data_disk = if direct {
|
||||||
|
format!("path={vmdk_path_str},image_type=vmdk,direct=on")
|
||||||
|
} else {
|
||||||
|
format!("path={vmdk_path_str},image_type=vmdk")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cloud_child = GuestCommand::new(guest)
|
||||||
|
.default_cpus()
|
||||||
|
.default_memory()
|
||||||
|
.default_kernel_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(),
|
||||||
|
data_disk.as_str(),
|
||||||
|
])
|
||||||
|
.default_net()
|
||||||
|
.capture_output()
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let r = panic::catch_unwind(|| {
|
||||||
|
guest.wait_vm_boot().unwrap();
|
||||||
|
|
||||||
|
// The twoGbMaxExtentFlat data disk shows up as vdc.
|
||||||
|
assert_eq!(
|
||||||
|
guest
|
||||||
|
.ssh_command("lsblk | grep -c vdc")
|
||||||
|
.unwrap()
|
||||||
|
.trim()
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write a random pattern that spans the 2 GiB extent boundary.
|
||||||
|
guest
|
||||||
|
.ssh_command(&format!(
|
||||||
|
"sudo dd if=/dev/urandom of=/tmp/pattern bs={BS} count={count} && \
|
||||||
|
sudo dd if=/tmp/pattern of=/dev/vdc bs={BS} count={count} seek={seek} \
|
||||||
|
oflag=direct conv=fsync && \
|
||||||
|
sudo dd if=/dev/vdc of=/tmp/readback bs={BS} count={count} skip={seek} \
|
||||||
|
iflag=direct && \
|
||||||
|
cmp /tmp/pattern /tmp/readback",
|
||||||
|
))
|
||||||
|
.expect("extent-spanning direct IO round trip across the 2 GiB boundary failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
kill_child(&mut cloud_child);
|
||||||
|
let output = cloud_child.wait_with_output().unwrap();
|
||||||
|
|
||||||
|
handle_child_output(r, &output);
|
||||||
|
|
||||||
|
disk_check_consistency(vmdk_path_str, None);
|
||||||
|
}
|
||||||
|
|
||||||
fn vhdx_image_size(disk_name: &str) -> u64 {
|
fn vhdx_image_size(disk_name: &str) -> u64 {
|
||||||
fs::File::open(disk_name)
|
fs::File::open(disk_name)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -1597,7 +1597,7 @@ mod common_parallel {
|
|||||||
ImageType::Qcow2 => ("qcow2", &[]),
|
ImageType::Qcow2 => ("qcow2", &[]),
|
||||||
ImageType::FixedVhd => ("vpc", &["-o", "subformat=fixed"]),
|
ImageType::FixedVhd => ("vpc", &["-o", "subformat=fixed"]),
|
||||||
ImageType::Vhdx => ("vhdx", &[]),
|
ImageType::Vhdx => ("vhdx", &[]),
|
||||||
ImageType::FlatVmdk => panic!("unsupported image_type {image_type}"),
|
ImageType::FlatVmdk => ("vmdk", &["-o", "subformat=monolithicFlat"]),
|
||||||
ImageType::Unknown => panic!("unsupported image_type {image_type}"),
|
ImageType::Unknown => panic!("unsupported image_type {image_type}"),
|
||||||
};
|
};
|
||||||
let image_type_str = image_type.to_string();
|
let image_type_str = image_type.to_string();
|
||||||
@@ -1734,6 +1734,11 @@ mod common_parallel {
|
|||||||
_test_virtio_block_direct_io_data_disk_4k(ImageType::Vhdx);
|
_test_virtio_block_direct_io_data_disk_4k(ImageType::Vhdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_direct_io_data_disk_4k_vmdk() {
|
||||||
|
_test_virtio_block_direct_io_data_disk_4k(ImageType::FlatVmdk);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() {
|
fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() {
|
||||||
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
|
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
|
||||||
@@ -2044,6 +2049,42 @@ mod common_parallel {
|
|||||||
_test_virtio_block_dynamic_vhdx_expand(&guest);
|
_test_virtio_block_dynamic_vhdx_expand(&guest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_vmdk_monolithic_flat() {
|
||||||
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
||||||
|
_test_virtio_block_vmdk(&guest, "monolithicFlat");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_vmdk_two_gb_max_extent_flat() {
|
||||||
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
||||||
|
_test_virtio_block_vmdk(&guest, "twoGbMaxExtentFlat");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_vmdk_monolithic_flat_enospc() {
|
||||||
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
||||||
|
_test_virtio_block_vmdk_enospc(&guest, "monolithicFlat");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_vmdk_two_gb_max_extent_flat_enospc() {
|
||||||
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
||||||
|
_test_virtio_block_vmdk_enospc(&guest, "twoGbMaxExtentFlat");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_vmdk_two_gb_max_extent_flat_spanning() {
|
||||||
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
||||||
|
_test_virtio_block_vmdk_extent_spanning(&guest, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtio_block_vmdk_two_gb_max_extent_flat_spanning_direct() {
|
||||||
|
let guest = basic_regular_guest!(JAMMY_IMAGE_NAME);
|
||||||
|
_test_virtio_block_vmdk_extent_spanning(&guest, true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_virtio_block_direct_and_firmware() {
|
fn test_virtio_block_direct_and_firmware() {
|
||||||
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
||||||
|
|||||||
Reference in New Issue
Block a user