From 27ee36449cc86dcae3dca16f2df51ed56153807b Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Thu, 23 Apr 2026 16:40:49 +0200 Subject: [PATCH] block: qcow: Add unified QcowDisk wrapper Introduce QcowDisk, a unified DiskFile implementation for QCOW2 disk images that handles backend selection at runtime via a use_io_uring flag, matching the pattern used by FixedVhdDisk. The wrapper delegates to QcowSync or QcowAsync based on the flag and includes a compile time guard that returns an error when io_uring is requested but the feature is not enabled. Signed-off-by: Anatol Belski --- block/src/lib.rs | 1 + block/src/qcow_async.rs | 2 +- block/src/qcow_disk.rs | 188 ++++++++++++++++++++++++++++++++++++++++ block/src/qcow_sync.rs | 2 +- 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 block/src/qcow_disk.rs diff --git a/block/src/lib.rs b/block/src/lib.rs index cd87875d5..d86b40129 100644 --- a/block/src/lib.rs +++ b/block/src/lib.rs @@ -23,6 +23,7 @@ pub mod qcow; #[cfg(feature = "io_uring")] pub mod qcow_async; pub(crate) mod qcow_common; +pub mod qcow_disk; pub mod qcow_sync; #[cfg(feature = "io_uring")] pub(crate) mod raw_async; diff --git a/block/src/qcow_async.rs b/block/src/qcow_async.rs index 8f58b4ba1..47e15eabe 100644 --- a/block/src/qcow_async.rs +++ b/block/src/qcow_async.rs @@ -186,7 +186,7 @@ pub struct QcowAsync { } impl QcowAsync { - fn new( + pub(crate) fn new( metadata: Arc, data_file: QcowRawFile, backing_file: Option>, diff --git a/block/src/qcow_disk.rs b/block/src/qcow_disk.rs new file mode 100644 index 000000000..effea82af --- /dev/null +++ b/block/src/qcow_disk.rs @@ -0,0 +1,188 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::os::unix::io::AsRawFd; +use std::sync::Arc; +use std::{fmt, io}; + +use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; +use crate::disk_file; +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::qcow::backing::shared_backing_from; +use crate::qcow::metadata::{BackingRead, QcowMetadata}; +use crate::qcow::qcow_raw_file::QcowRawFile; +use crate::qcow::{MAX_NESTING_DEPTH, RawFile, parse_qcow}; +#[cfg(feature = "io_uring")] +use crate::qcow_async::QcowAsync; +use crate::qcow_sync::QcowSync; + +/// Unified DiskFile wrapper for QCOW2 disk images. +/// +/// Holds the in memory QCOW2 metadata, the data file, and an optional +/// backing file. The metadata is wrapped in an `Arc` because +/// [`QcowSync`] and [`QcowAsync`] I/O workers receive a clone when +/// they are created via [`create_async_io`](DiskFile::create_async_io). +/// The backing file is likewise shared with workers through an `Arc`. +/// +/// The `sparse` flag controls whether the image advertises discard +/// support to the guest. The `use_io_uring` flag selects between the +/// [`QcowSync`] and [`QcowAsync`] I/O backends. Both are recorded at +/// construction time and propagated through [`try_clone`](DiskFile::try_clone). +pub struct QcowDisk { + metadata: Arc, + backing_file: Option>, + sparse: bool, + data_raw_file: QcowRawFile, + use_io_uring: bool, +} + +impl fmt::Debug for QcowDisk { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("QcowDisk") + .field("sparse", &self.sparse) + .field("has_backing", &self.backing_file.is_some()) + .field("use_io_uring", &self.use_io_uring) + .finish_non_exhaustive() + } +} + +impl QcowDisk { + pub fn new( + file: File, + direct_io: bool, + backing_files: bool, + sparse: bool, + use_io_uring: bool, + ) -> BlockResult { + #[cfg(not(feature = "io_uring"))] + if use_io_uring { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + DiskFileError::NewAsyncIo(io::Error::other( + "io_uring requested but feature is not enabled", + )), + )); + } + + let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 }; + let raw_file = RawFile::new(file, direct_io); + let (inner, backing_file, sparse) = parse_qcow(raw_file, max_nesting_depth, sparse) + .map_err(|e| { + let e = if !backing_files && matches!(e.kind(), BlockErrorKind::Overflow) { + e.with_kind(BlockErrorKind::UnsupportedFeature) + } else { + e + }; + e.with_op(ErrorOp::Open) + })?; + let data_raw_file = inner.raw_file.clone(); + Ok(QcowDisk { + metadata: Arc::new(QcowMetadata::new(inner)), + backing_file: backing_file.map(shared_backing_from).transpose()?, + sparse, + data_raw_file, + use_io_uring, + }) + } +} + +impl Drop for QcowDisk { + fn drop(&mut self) { + self.metadata.shutdown(); + } +} + +impl disk_file::DiskSize for QcowDisk { + fn logical_size(&self) -> BlockResult { + Ok(self.metadata.virtual_size()) + } +} + +impl disk_file::PhysicalSize for QcowDisk { + fn physical_size(&self) -> BlockResult { + Ok(self.data_raw_file.physical_size()?) + } +} + +impl disk_file::DiskFd for QcowDisk { + fn fd(&self) -> BorrowedDiskFd<'_> { + BorrowedDiskFd::new(self.data_raw_file.as_raw_fd()) + } +} + +impl disk_file::Geometry for QcowDisk {} + +impl disk_file::SparseCapable for QcowDisk { + fn supports_sparse_operations(&self) -> bool { + true + } + + fn supports_zero_flag(&self) -> bool { + true + } +} + +impl disk_file::Resizable for QcowDisk { + fn resize(&mut self, size: u64) -> BlockResult<()> { + if self.backing_file.is_some() { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + DiskFileError::ResizeError(io::Error::other( + "resize not supported with backing files", + )), + ) + .with_op(ErrorOp::Resize)); + } + self.metadata.resize(size).map_err(|e| { + BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)) + .with_op(ErrorOp::Resize) + }) + } +} + +impl disk_file::DiskFile for QcowDisk {} + +impl disk_file::AsyncDiskFile for QcowDisk { + fn try_clone(&self) -> BlockResult> { + Ok(Box::new(QcowDisk { + metadata: Arc::clone(&self.metadata), + backing_file: self.backing_file.as_ref().map(Arc::clone), + sparse: self.sparse, + data_raw_file: self.data_raw_file.clone(), + use_io_uring: self.use_io_uring, + })) + } + + fn create_async_io(&self, ring_depth: u32) -> BlockResult> { + if self.use_io_uring { + #[cfg(feature = "io_uring")] + { + return Ok(Box::new( + QcowAsync::new( + Arc::clone(&self.metadata), + self.data_raw_file.clone(), + self.backing_file.as_ref().map(Arc::clone), + self.sparse, + ring_depth, + ) + .map_err(|e| { + BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e)) + })?, + )); + } + + #[cfg(not(feature = "io_uring"))] + unreachable!("use_io_uring is set but io_uring feature is not enabled"); + } + + let _ = ring_depth; + Ok(Box::new(QcowSync::new( + Arc::clone(&self.metadata), + self.data_raw_file.clone(), + self.backing_file.as_ref().map(Arc::clone), + self.sparse, + ))) + } +} diff --git a/block/src/qcow_sync.rs b/block/src/qcow_sync.rs index 4d722728c..8bbe01a97 100644 --- a/block/src/qcow_sync.rs +++ b/block/src/qcow_sync.rs @@ -165,7 +165,7 @@ pub struct QcowSync { } impl QcowSync { - fn new( + pub(crate) fn new( metadata: Arc, data_file: QcowRawFile, backing_file: Option>,