Skip to content

Commit 93393e2

Browse files
committed
Consistently access qcow2 with --force-share
We only want to read metadata, which should be safe to do from concurrent processes. This was the cause of test flakes. Signed-off-by: Colin Walters <[email protected]>
1 parent 6abffaa commit 93393e2

File tree

5 files changed

+78
-69
lines changed

5 files changed

+78
-69
lines changed

crates/kit/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
//! bcvk library - exposes internal modules for testing
22
3+
pub mod qemu_img;
34
pub mod xml_utils;

crates/kit/src/libvirt/base_disks.rs

Lines changed: 30 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -247,27 +247,8 @@ pub fn clone_from_base(
247247
);
248248

249249
// Get the virtual size of the base disk to use for the new volume
250-
let qemu_img_output = std::process::Command::new("qemu-img")
251-
.args(&["info", "--output=json", base_disk_path.as_str()])
252-
.output()
253-
.with_context(|| format!("Failed to get base disk info: {:?}", base_disk_path))?;
254-
255-
if !qemu_img_output.status.success() {
256-
let stderr = String::from_utf8(qemu_img_output.stderr)
257-
.with_context(|| "Invalid UTF-8 in qemu-img stderr")?;
258-
return Err(color_eyre::eyre::eyre!(
259-
"Failed to query base disk size: {}",
260-
stderr
261-
));
262-
}
263-
264-
// Parse JSON directly from bytes
265-
let info: serde_json::Value = serde_json::from_slice(&qemu_img_output.stdout)
266-
.with_context(|| "Failed to parse qemu-img info JSON")?;
267-
268-
let virtual_size = info["virtual-size"]
269-
.as_u64()
270-
.ok_or_else(|| color_eyre::eyre::eyre!("Missing virtual-size in qemu-img output"))?;
250+
let info = crate::qemu_img::info(base_disk_path)?;
251+
let virtual_size = info.virtual_size;
271252

272253
// Create volume with backing file using vol-create-as
273254
// This creates a qcow2 image with the base disk as backing file (proper CoW)
@@ -448,33 +429,27 @@ fn count_base_disk_references(base_disk: &Utf8Path, vm_disks: &[&Utf8PathBuf]) -
448429

449430
for vm_disk in vm_disks {
450431
// Use qemu-img info with --force-share to allow reading even if disk is locked by a running VM
451-
let output = std::process::Command::new("qemu-img")
452-
.args(&["info", "--force-share", "--output=json", vm_disk.as_str()])
453-
.output()
454-
.with_context(|| format!("Failed to run qemu-img info on {:?}", vm_disk))?;
455-
456-
if !output.status.success() {
457-
// If we can't read the disk, skip it for counting purposes
458-
// (We're conservative in check_base_disk_referenced but here we just want a count)
459-
debug!(
460-
"Warning: Could not read disk info for {:?}, skipping for reference count",
461-
vm_disk
462-
);
463-
continue;
464-
}
465-
466-
// Parse JSON directly from bytes
467-
let info: serde_json::Value = serde_json::from_slice(&output.stdout)
468-
.with_context(|| format!("Failed to parse qemu-img JSON output for {:?}", vm_disk))?;
432+
let info = match crate::qemu_img::info(vm_disk) {
433+
Ok(info) => info,
434+
Err(_) => {
435+
// If we can't read the disk, skip it for counting purposes
436+
// (We're conservative in check_base_disk_referenced but here we just want a count)
437+
debug!(
438+
"Warning: Could not read disk info for {:?}, skipping for reference count",
439+
vm_disk
440+
);
441+
continue;
442+
}
443+
};
469444

470445
// Check both "backing-filename" and "full-backing-filename" fields
471-
if let Some(backing_file) = info["backing-filename"].as_str() {
446+
if let Some(backing_file) = &info.backing_filename {
472447
if backing_file.contains(base_disk_name) {
473448
count += 1;
474449
continue;
475450
}
476451
}
477-
if let Some(backing_file) = info["full-backing-filename"].as_str() {
452+
if let Some(backing_file) = &info.full_backing_filename {
478453
if backing_file.contains(base_disk_name) {
479454
count += 1;
480455
}
@@ -490,29 +465,21 @@ fn check_base_disk_referenced(base_disk: &Utf8Path, vm_disks: &[&Utf8PathBuf]) -
490465

491466
for vm_disk in vm_disks {
492467
// Use qemu-img info with --force-share to allow reading even if disk is locked by a running VM
493-
let output = std::process::Command::new("qemu-img")
494-
.args(&["info", "--force-share", "--output=json", vm_disk.as_str()])
495-
.output()
496-
.with_context(|| format!("Failed to run qemu-img info on {:?}", vm_disk))?;
497-
498-
if !output.status.success() {
499-
// If we can't read the disk info, be conservative and assume it DOES reference this base
500-
// This prevents accidentally pruning base disks that are in use
501-
let stderr = String::from_utf8(output.stderr)
502-
.with_context(|| format!("Invalid UTF-8 in qemu-img stderr for {:?}", vm_disk))?;
503-
debug!(
504-
"Warning: Could not read disk info for {:?}, conservatively assuming it references base disk: {}",
505-
vm_disk, stderr
506-
);
507-
return Ok(true);
508-
}
509-
510-
// Parse JSON directly from bytes
511-
let info: serde_json::Value = serde_json::from_slice(&output.stdout)
512-
.with_context(|| format!("Failed to parse qemu-img JSON output for {:?}", vm_disk))?;
468+
let info = match crate::qemu_img::info(vm_disk) {
469+
Ok(info) => info,
470+
Err(e) => {
471+
// If we can't read the disk info, be conservative and assume it DOES reference this base
472+
// This prevents accidentally pruning base disks that are in use
473+
debug!(
474+
"Warning: Could not read disk info for {:?}, conservatively assuming it references base disk: {}",
475+
vm_disk, e
476+
);
477+
return Ok(true);
478+
}
479+
};
513480

514481
// Check both "backing-filename" and "full-backing-filename" fields
515-
if let Some(backing_file) = info["backing-filename"].as_str() {
482+
if let Some(backing_file) = &info.backing_filename {
516483
if backing_file.contains(base_disk_name) {
517484
debug!(
518485
"Found backing file reference: {:?} -> {:?}",
@@ -521,7 +488,7 @@ fn check_base_disk_referenced(base_disk: &Utf8Path, vm_disks: &[&Utf8PathBuf]) -
521488
return Ok(true);
522489
}
523490
}
524-
if let Some(backing_file) = info["full-backing-filename"].as_str() {
491+
if let Some(backing_file) = &info.full_backing_filename {
525492
if backing_file.contains(base_disk_name) {
526493
debug!(
527494
"Found full backing file reference: {:?} -> {:?}",

crates/kit/src/libvirt/run.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -562,10 +562,7 @@ mod tests {
562562
fn test_parse_volume_mount_nonexistent_host() {
563563
let result = parse_volume_mount("/nonexistent/path/that/does/not/exist:mytag");
564564
assert!(result.is_err());
565-
assert!(result
566-
.unwrap_err()
567-
.to_string()
568-
.contains("does not exist"));
565+
assert!(result.unwrap_err().to_string().contains("does not exist"));
569566
}
570567
}
571568

@@ -692,8 +689,7 @@ fn create_libvirt_domain_from_disk(
692689
readonly: false,
693690
};
694691

695-
domain_builder = domain_builder
696-
.with_virtiofs_filesystem(virtiofs_fs);
692+
domain_builder = domain_builder.with_virtiofs_filesystem(virtiofs_fs);
697693
}
698694
}
699695

crates/kit/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod libvirt_upload_disk;
2323
mod podman;
2424
#[allow(dead_code)]
2525
mod qemu;
26+
mod qemu_img;
2627
mod run_ephemeral;
2728
mod run_ephemeral_ssh;
2829
mod ssh;

crates/kit/src/qemu_img.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! Helper functions for interacting with qemu-img
2+
3+
use camino::Utf8Path;
4+
use color_eyre::{eyre::Context, Result};
5+
use serde::Deserialize;
6+
use std::process::Command;
7+
8+
/// Information returned by `qemu-img info --output=json`
9+
#[derive(Debug, Deserialize)]
10+
#[serde(rename_all = "kebab-case")]
11+
#[allow(dead_code)]
12+
pub struct QemuImgInfo {
13+
pub virtual_size: u64,
14+
pub filename: String,
15+
pub format: String,
16+
pub actual_size: Option<u64>,
17+
pub cluster_size: Option<u64>,
18+
pub backing_filename: Option<String>,
19+
pub full_backing_filename: Option<String>,
20+
pub dirty_flag: Option<bool>,
21+
}
22+
23+
/// Run `qemu-img info --force-share --output=json` on a disk image
24+
///
25+
/// The `--force-share` flag allows reading disk info even when the image
26+
/// is locked by a running VM.
27+
pub fn info(path: &Utf8Path) -> Result<QemuImgInfo> {
28+
let output = Command::new("qemu-img")
29+
.args(["info", "--force-share", "--output=json", path.as_str()])
30+
.output()
31+
.with_context(|| format!("Failed to run qemu-img info on {:?}", path))?;
32+
33+
if !output.status.success() {
34+
let stderr = String::from_utf8_lossy(&output.stderr);
35+
return Err(color_eyre::eyre::eyre!(
36+
"qemu-img info failed for {:?}: {}",
37+
path,
38+
stderr
39+
));
40+
}
41+
42+
serde_json::from_slice(&output.stdout)
43+
.with_context(|| format!("Failed to parse qemu-img info JSON for {:?}", path))
44+
}

0 commit comments

Comments
 (0)