tests: qcow: Add dirty bit integration tests

Add tests to verify dirty bit is set while VM runs and cleared on
clean shutdown. As part of it, ensure graceful shutdown when OS
disk verification requires consistent image state.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2026-01-25 21:35:47 +01:00
committed by Rob Bradford
parent 82dc9bfaee
commit 919cce9b58

View File

@@ -2528,7 +2528,7 @@ EOF
mod common_parallel {
use std::cmp;
use std::fs::{File, OpenOptions};
use std::fs::{File, OpenOptions, copy};
use std::io::{self, SeekFrom};
use crate::*;
@@ -3499,7 +3499,13 @@ mod common_parallel {
);
});
let _ = cloud_child.kill();
if verify_os_disk {
// Use clean shutdown to allow cloud-hypervisor to clear
// the dirty bit in the QCOW2 v3 image.
kill_child(&mut cloud_child);
} else {
let _ = cloud_child.kill();
}
let output = cloud_child.wait_with_output().unwrap();
handle_child_output(r, &output);
@@ -3537,12 +3543,25 @@ mod common_parallel {
}
fn get_image_info(path: &std::path::Path) -> Option<serde_json::Value> {
let output = run_qemu_img(path, &["info", "--output=json"]);
let output = run_qemu_img(path, &["info", "-U", "--output=json"]);
output.status.success().then(|| ())?;
serde_json::from_slice(&output.stdout).ok()
}
fn check_dirty_flag(path: &std::path::Path) -> Result<Option<bool>, String> {
let info = get_image_info(path)
.ok_or_else(|| format!("qemu-img info failed for {}", path.display()))?;
if info["format"].as_str() != Some("qcow2") {
return Ok(None);
}
// QCOW2 v3 has compat "1.1", v2 has "0.10" and doesn't support dirty flag
if info["format-specific"]["data"]["compat"].as_str() != Some("1.1") {
return Ok(None);
}
Ok(info["dirty-flag"].as_bool())
}
fn resolve_disk_path(path_or_image_name: impl AsRef<std::path::Path>) -> std::path::PathBuf {
if path_or_image_name.as_ref().exists() {
// A full path is provided
@@ -3662,9 +3681,11 @@ mod common_parallel {
/// It takes either a full path to the image or just the name of
/// the image located in the `workloads` directory.
///
/// For qcow2 images with backing files, also verifies the backing file
/// For QCOW2 images with backing files, also verifies the backing file
/// integrity and checks that the backing file hasn't been modified
/// during the test.
///
/// For QCOW2 v3 images, also verifies the dirty bit is cleared.
fn disk_check_consistency(
path_or_image_name: impl AsRef<std::path::Path>,
initial_backing_checksum: Option<(std::path::PathBuf, String, u32)>,
@@ -3678,6 +3699,14 @@ mod common_parallel {
String::from_utf8_lossy(&output.stderr)
);
match check_dirty_flag(&path) {
Ok(Some(dirty)) => {
assert!(!dirty, "QCOW2 image shutdown unclean");
}
Ok(None) => {} // Not a QCOW2 v3 image, skip dirty flag check
Err(e) => panic!("Failed to check dirty flag: {e}"),
}
if let Some((backing_path, format, initial_checksum)) = initial_backing_checksum {
if format.parse::<block::qcow::ImageType>().ok() != Some(block::qcow::ImageType::Raw) {
let output = run_qemu_img(&backing_path, &["check"]);
@@ -3733,6 +3762,128 @@ mod common_parallel {
_test_virtio_block(JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE, false, false, true);
}
#[test]
fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test-dirty.qcow2");
let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
copy(original_image, &test_image_path).expect("Failed to copy qcow2 image");
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(false),
"Image should start with dirty bit cleared"
);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=1"])
.args(["--memory", "size=512M"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
&format!("path={}", test_image_path.to_str().unwrap()),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot(None).unwrap();
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(true),
"Dirty bit should be set while VM is running"
);
});
if r.is_err() {
let _ = child.kill();
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
return;
}
// Simulate unclean shutdown with SIGKILL
let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) };
let _ = child.wait();
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(true),
"Dirty bit should remain set after unclean shutdown"
);
}
#[test]
fn test_virtio_block_qcow2_dirty_bit_clean_shutdown() {
let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string());
let guest = Guest::new(Box::new(disk_config));
let kernel_path = direct_kernel_boot_path();
let test_image_path = guest.tmp_dir.as_path().join("test-dirty.qcow2");
let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap();
copy(original_image, &test_image_path).expect("Failed to copy qcow2 image");
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(false),
"Image should start with dirty bit cleared"
);
let mut child = GuestCommand::new(&guest)
.args(["--cpus", "boot=1"])
.args(["--memory", "size=512M"])
.args(["--kernel", kernel_path.to_str().unwrap()])
.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE])
.args([
"--disk",
&format!("path={}", test_image_path.to_str().unwrap()),
&format!(
"path={}",
guest.disk_config.disk(DiskType::CloudInit).unwrap()
),
])
.default_net()
.capture_output()
.spawn()
.unwrap();
let r = std::panic::catch_unwind(|| {
guest.wait_vm_boot(None).unwrap();
assert_eq!(
check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"),
Some(true),
"Dirty bit should be set while VM is running"
);
});
// Clean shutdown using SIGTERM
kill_child(&mut child);
if r.is_err() {
let output = child.wait_with_output().unwrap();
handle_child_output(r, &output);
return;
}
let _ = child.wait();
disk_check_consistency(&test_image_path, None);
}
#[test]
fn test_virtio_block_vhd() {
let mut workload_path = dirs::home_dir().unwrap();