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);
|
||||
}
|
||||
|
||||
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 {
|
||||
fs::File::open(disk_name)
|
||||
.unwrap()
|
||||
|
||||
@@ -1597,7 +1597,7 @@ mod common_parallel {
|
||||
ImageType::Qcow2 => ("qcow2", &[]),
|
||||
ImageType::FixedVhd => ("vpc", &["-o", "subformat=fixed"]),
|
||||
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}"),
|
||||
};
|
||||
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]
|
||||
fn test_virtio_block_direct_io_data_disk_4k_vmdk() {
|
||||
_test_virtio_block_direct_io_data_disk_4k(ImageType::FlatVmdk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() {
|
||||
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]
|
||||
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]
|
||||
fn test_virtio_block_direct_and_firmware() {
|
||||
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string());
|
||||
|
||||
Reference in New Issue
Block a user