block: qcow: Add raw backing file support
Add support for raw backing files in addition to qcow2 backing files. This enables QCOW2 overlays to use raw images as their backing store. The backing file format is auto-detected when not specified, using the existing detect_image_type() function. Add backing_file_format field to QcowHeader to store the format type, which will be populated from header extensions by a subsequent patch. Modify new_from_backing() to accept a backing_format parameter, consolidating support for both raw and qcow2 backing files in a single function. The backing_file_size parameter allows overlay creation without opening the backing file multiple times. Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
parent
cb495959a8
commit
b3922dbd2c
1 changed files with 160 additions and 56 deletions
|
|
@ -11,11 +11,12 @@ mod refcount;
|
|||
mod vec_cache;
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::mem::size_of;
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::str;
|
||||
use std::str::{self, FromStr};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use libc::{EINVAL, EIO, ENOSPC};
|
||||
|
|
@ -111,6 +112,8 @@ pub enum Error {
|
|||
TooManyL1Entries(u64),
|
||||
#[error("Ref count table too large: {0}")]
|
||||
TooManyRefcounts(u64),
|
||||
#[error("Unsupported backing file format: {0}")]
|
||||
UnsupportedBackingFileFormat(String),
|
||||
#[error("Unsupported compression type")]
|
||||
UnsupportedCompressionType,
|
||||
#[error("Unsupported refcount order")]
|
||||
|
|
@ -125,18 +128,46 @@ pub enum Error {
|
|||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ImageType {
|
||||
Raw,
|
||||
Qcow2,
|
||||
}
|
||||
|
||||
impl Display for ImageType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
ImageType::Raw => write!(f, "raw"),
|
||||
ImageType::Qcow2 => write!(f, "qcow2"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ImageType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"raw" => Ok(ImageType::Raw),
|
||||
"qcow2" => Ok(ImageType::Qcow2),
|
||||
_ => Err(Error::UnsupportedBackingFileFormat(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CompressionType {
|
||||
Zlib,
|
||||
Zstd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BackingFileConfig {
|
||||
pub path: String,
|
||||
// If this is None, we will autodetect it.
|
||||
pub format: Option<ImageType>,
|
||||
}
|
||||
|
||||
// Maximum data size supported.
|
||||
const MAX_QCOW_FILE_SIZE: u64 = 0x01 << 44; // 16 TB.
|
||||
|
||||
|
|
@ -243,7 +274,7 @@ pub struct QcowHeader {
|
|||
pub compression_type: CompressionType,
|
||||
|
||||
// Post-header entries
|
||||
pub backing_file_path: Option<String>,
|
||||
pub backing_file: Option<BackingFileConfig>,
|
||||
}
|
||||
|
||||
impl QcowHeader {
|
||||
|
|
@ -307,7 +338,7 @@ impl QcowHeader {
|
|||
read_u32_from_file(f)?
|
||||
},
|
||||
compression_type: CompressionType::Zlib,
|
||||
backing_file_path: None,
|
||||
backing_file: None,
|
||||
};
|
||||
if version == 3 && header.header_size > V3_BARE_HEADER_SIZE {
|
||||
let raw_compression_type = read_u64_from_file(f)? >> (64 - 8);
|
||||
|
|
@ -328,10 +359,9 @@ impl QcowHeader {
|
|||
let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize];
|
||||
f.read_exact(&mut backing_file_name_bytes)
|
||||
.map_err(Error::ReadingHeader)?;
|
||||
header.backing_file_path = Some(
|
||||
String::from_utf8(backing_file_name_bytes)
|
||||
.map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?,
|
||||
);
|
||||
let path = String::from_utf8(backing_file_name_bytes)
|
||||
.map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?;
|
||||
header.backing_file = Some(BackingFileConfig { path, format: None });
|
||||
}
|
||||
Ok(header)
|
||||
}
|
||||
|
|
@ -407,7 +437,10 @@ impl QcowHeader {
|
|||
refcount_order: DEFAULT_REFCOUNT_ORDER,
|
||||
header_size,
|
||||
compression_type: CompressionType::Zlib,
|
||||
backing_file_path: backing_file.map(String::from),
|
||||
backing_file: backing_file.map(|path| BackingFileConfig {
|
||||
path: String::from(path),
|
||||
format: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -449,7 +482,7 @@ impl QcowHeader {
|
|||
write_u32_to_file(file, 0)?; // length of header extension data: 0
|
||||
}
|
||||
|
||||
if let Some(backing_file_path) = self.backing_file_path.as_ref() {
|
||||
if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) {
|
||||
write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?;
|
||||
}
|
||||
|
||||
|
|
@ -479,6 +512,92 @@ fn max_refcount_clusters(refcount_order: u32, cluster_size: u32, num_clusters: u
|
|||
for_data + for_refcounts
|
||||
}
|
||||
|
||||
trait BackingFileOps: Send + Seek + Read {
|
||||
fn read_at(&mut self, address: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
self.seek(SeekFrom::Start(address))?;
|
||||
self.read_exact(buf)
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn BackingFileOps>;
|
||||
}
|
||||
|
||||
impl BackingFileOps for QcowFile {
|
||||
fn clone_box(&self) -> Box<dyn BackingFileOps> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl BackingFileOps for RawFile {
|
||||
fn clone_box(&self) -> Box<dyn BackingFileOps> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Backing file wrapper
|
||||
struct BackingFile {
|
||||
inner: Box<dyn BackingFileOps>,
|
||||
}
|
||||
|
||||
impl BackingFile {
|
||||
fn new(
|
||||
backing_file_config: Option<&BackingFileConfig>,
|
||||
direct_io: bool,
|
||||
max_nesting_depth: u32,
|
||||
) -> Result<Option<Self>> {
|
||||
let Some(config) = backing_file_config else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Check nesting depth - applies to any backing file
|
||||
if max_nesting_depth == 0 {
|
||||
return Err(Error::MaxNestingDepthExceeded);
|
||||
}
|
||||
|
||||
let backing_raw_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(&config.path)
|
||||
.map_err(Error::BackingFileIo)?;
|
||||
|
||||
let mut raw_file = RawFile::new(backing_raw_file, direct_io);
|
||||
|
||||
// Determine backing file format from header extension or auto-detect
|
||||
let backing_format = match config.format {
|
||||
Some(format) => format,
|
||||
None => detect_image_type(&mut raw_file)?,
|
||||
};
|
||||
|
||||
let inner: Box<dyn BackingFileOps> = match backing_format {
|
||||
ImageType::Raw => Box::new(raw_file),
|
||||
ImageType::Qcow2 => {
|
||||
let backing_qcow =
|
||||
QcowFile::from_with_nesting_depth(raw_file, max_nesting_depth - 1)
|
||||
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
|
||||
Box::new(backing_qcow)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(Self { inner }))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_at(&mut self, address: u64, buf: &mut [u8]) -> std::io::Result<()> {
|
||||
self.inner.read_at(address, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BackingFile {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone_box(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for BackingFile {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.debug_struct("BackingFile").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a qcow2 file. This is a sparse file format maintained by the qemu project.
|
||||
/// Full documentation of the format can be found in the qemu repository.
|
||||
///
|
||||
|
|
@ -509,7 +628,7 @@ pub struct QcowFile {
|
|||
// List of unreferenced clusters available to be used. unref clusters become available once the
|
||||
// removal of references to them have been synced to disk.
|
||||
avail_clusters: Vec<u64>,
|
||||
backing_file: Option<Box<Self>>,
|
||||
backing_file: Option<BackingFile>,
|
||||
}
|
||||
|
||||
impl QcowFile {
|
||||
|
|
@ -548,24 +667,8 @@ impl QcowFile {
|
|||
|
||||
let direct_io = file.is_direct();
|
||||
|
||||
let backing_file = if let Some(backing_file_path) = header.backing_file_path.as_ref() {
|
||||
if max_nesting_depth == 0 {
|
||||
return Err(Error::MaxNestingDepthExceeded);
|
||||
}
|
||||
let path = backing_file_path.clone();
|
||||
let backing_raw_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(path)
|
||||
.map_err(Error::BackingFileIo)?;
|
||||
let backing_file = Self::from_with_nesting_depth(
|
||||
RawFile::new(backing_raw_file, direct_io),
|
||||
max_nesting_depth - 1,
|
||||
)
|
||||
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
|
||||
Some(Box::new(backing_file))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let backing_file =
|
||||
BackingFile::new(header.backing_file.as_ref(), direct_io, max_nesting_depth)?;
|
||||
|
||||
// Only support two byte refcounts.
|
||||
let refcount_bits: u64 = 0x01u64
|
||||
|
|
@ -692,28 +795,23 @@ impl QcowFile {
|
|||
QcowFile::new_from_header(file, &header)
|
||||
}
|
||||
|
||||
/// Creates a new QcowFile at the given path.
|
||||
/// Creates a new QcowFile at the given path with a backing file.
|
||||
pub fn new_from_backing(
|
||||
file: RawFile,
|
||||
version: u32,
|
||||
backing_file_name: &str,
|
||||
backing_file_max_nesting_depth: u32,
|
||||
backing_file_size: u64,
|
||||
backing_config: &BackingFileConfig,
|
||||
) -> Result<QcowFile> {
|
||||
let direct_io = file.is_direct();
|
||||
let backing_raw_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(backing_file_name)
|
||||
.map_err(Error::BackingFileIo)?;
|
||||
let backing_file = Self::from_with_nesting_depth(
|
||||
RawFile::new(backing_raw_file, direct_io),
|
||||
backing_file_max_nesting_depth,
|
||||
)
|
||||
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
|
||||
let size = backing_file.virtual_size();
|
||||
let header = QcowHeader::create_for_size_and_path(version, size, Some(backing_file_name))?;
|
||||
let mut result = QcowFile::new_from_header(file, &header)?;
|
||||
result.backing_file = Some(Box::new(backing_file));
|
||||
Ok(result)
|
||||
let mut header = QcowHeader::create_for_size_and_path(
|
||||
version,
|
||||
backing_file_size,
|
||||
Some(&backing_config.path),
|
||||
)?;
|
||||
if let Some(backing_file) = &mut header.backing_file {
|
||||
backing_file.format = backing_config.format;
|
||||
}
|
||||
QcowFile::new_from_header(file, &header)
|
||||
// backing_file is loaded by new_from_header -> Self::from() based on the header
|
||||
}
|
||||
|
||||
fn new_from_header(mut file: RawFile, header: &QcowHeader) -> Result<QcowFile> {
|
||||
|
|
@ -741,7 +839,9 @@ impl QcowFile {
|
|||
}
|
||||
|
||||
pub fn set_backing_file(&mut self, backing: Option<Box<Self>>) {
|
||||
self.backing_file = backing;
|
||||
self.backing_file = backing.map(|b| BackingFile {
|
||||
inner: Box::new(*b),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the `QcowHeader` for this file.
|
||||
|
|
@ -1246,8 +1346,7 @@ impl QcowFile {
|
|||
let cluster_size = self.raw_file.cluster_size();
|
||||
let cluster_begin = address - (address % cluster_size);
|
||||
let mut cluster_data = vec![0u8; cluster_size as usize];
|
||||
backing.seek(SeekFrom::Start(cluster_begin))?;
|
||||
backing.read_exact(&mut cluster_data)?;
|
||||
backing.read_at(cluster_begin, &mut cluster_data)?;
|
||||
Some(cluster_data)
|
||||
} else {
|
||||
None
|
||||
|
|
@ -1664,8 +1763,7 @@ impl Read for QcowFile {
|
|||
if (self.file_read(curr_addr, count, &mut buf[nread..(nread + count)])?).is_some() {
|
||||
// Data is successfully read from the cluster
|
||||
} else if let Some(backing) = self.backing_file.as_mut() {
|
||||
backing.seek(SeekFrom::Start(curr_addr))?;
|
||||
backing.read_exact(&mut buf[nread..(nread + count)])?;
|
||||
backing.read_at(curr_addr, &mut buf[nread..(nread + count)])?;
|
||||
} else {
|
||||
// Previously unwritten region, return zeros
|
||||
for b in &mut buf[nread..(nread + count)] {
|
||||
|
|
@ -2147,10 +2245,13 @@ mod unit_tests {
|
|||
disk_file.rewind().unwrap();
|
||||
let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header.");
|
||||
assert_eq!(
|
||||
header.backing_file_path,
|
||||
header.backing_file.as_ref().map(|bf| bf.path.clone()),
|
||||
Some(String::from("/my/path/to/a/file"))
|
||||
);
|
||||
assert_eq!(read_header.backing_file_path, header.backing_file_path);
|
||||
assert_eq!(
|
||||
read_header.backing_file.as_ref().map(|bf| &bf.path),
|
||||
header.backing_file.as_ref().map(|bf| &bf.path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2164,10 +2265,13 @@ mod unit_tests {
|
|||
disk_file.rewind().unwrap();
|
||||
let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header.");
|
||||
assert_eq!(
|
||||
header.backing_file_path,
|
||||
header.backing_file.as_ref().map(|bf| bf.path.clone()),
|
||||
Some(String::from("/my/path/to/a/file"))
|
||||
);
|
||||
assert_eq!(read_header.backing_file_path, header.backing_file_path);
|
||||
assert_eq!(
|
||||
read_header.backing_file.as_ref().map(|bf| &bf.path),
|
||||
header.backing_file.as_ref().map(|bf| &bf.path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue