mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
block: Implement synchronous I/O engine for Flat VMDK
Implements synchronous I/O engine for flat VMDK backend. It uses an extent aware worker to map each request to one or more backing extents. The implementation supports extents opened with O_DIRECT using AlignedFile. Async backends of io_uring and AIO are unimplemented because requests spanning extents cannot be submitted with one fd + offset. Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
committed by
Wei Liu
parent
0769215d42
commit
e6fd5fefc4
293
block/src/formats/vmdk/engine_sync.rs
Normal file
293
block/src/formats/vmdk/engine_sync.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::sync::Arc;
|
||||
use std::{cmp, io};
|
||||
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::AlignedFile;
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, CompletionCommon,
|
||||
};
|
||||
use crate::formats::vmdk::flat::{ExtentAccess, VmdkExtent};
|
||||
|
||||
/// Synchronous, extent-aware I/O worker for flat VMDK images.
|
||||
///
|
||||
/// Maps each guest I/O request to one or more backing extents.
|
||||
///
|
||||
/// Async backends (io_uring/AIO) are not supported.
|
||||
pub(crate) struct FlatVmdkSync {
|
||||
extents: Arc<Vec<VmdkExtent>>,
|
||||
size: u64,
|
||||
completions: CompletionCommon,
|
||||
}
|
||||
|
||||
impl FlatVmdkSync {
|
||||
pub fn new(extents: Arc<Vec<VmdkExtent>>, size: u64) -> Self {
|
||||
FlatVmdkSync {
|
||||
extents,
|
||||
size,
|
||||
completions: CompletionCommon::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the extent containing virtual `offset`, or `None` if out of range.
|
||||
fn extent_at(&self, offset: u64) -> Option<&VmdkExtent> {
|
||||
self.extents
|
||||
.iter()
|
||||
.find(|e| offset >= e.virtual_start && offset < e.virtual_start + e.length)
|
||||
}
|
||||
|
||||
fn check_access(&self, start: u64, total: u64, is_read: bool) -> io::Result<()> {
|
||||
let end = start + total;
|
||||
let mut cur = start;
|
||||
while cur < end {
|
||||
let extent = self.extent_at(cur).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent")
|
||||
})?;
|
||||
match extent.access {
|
||||
ExtentAccess::NoAccess => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("VMDK extent at offset {cur} is NOACCESS; request rejected"),
|
||||
));
|
||||
}
|
||||
ExtentAccess::ReadOnly if !is_read => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("write to read-only VMDK extent at offset {cur} rejected"),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
cur = extent.virtual_start + extent.length;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Reads or writes a single contiguous segment of one extent through the
|
||||
// extent's `AlignedFile`.
|
||||
fn segment_io(
|
||||
file: &AlignedFile,
|
||||
file_offset: u64,
|
||||
op: &mut AsyncIoOperation,
|
||||
buf_start: usize,
|
||||
seg_len: usize,
|
||||
is_read: bool,
|
||||
) -> io::Result<usize> {
|
||||
// O_DIRECT unaligned
|
||||
if file.alignment() != 0 {
|
||||
return if is_read {
|
||||
file.read_unaligned(file_offset, seg_len, |data| {
|
||||
op.write_bytes_at(buf_start, data)
|
||||
})
|
||||
} else {
|
||||
file.write_unaligned(file_offset, seg_len, |data| {
|
||||
op.read_bytes_at(buf_start, data)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// Aligned & Buffered
|
||||
let mut buf = vec![0u8; seg_len];
|
||||
let mut done = 0usize;
|
||||
if is_read {
|
||||
while done < seg_len {
|
||||
match file.read_at(&mut buf[done..], file_offset + done as u64) {
|
||||
Ok(0) => break, // EOF: nothing more to read
|
||||
Ok(n) => done += n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
op.write_bytes_at(buf_start, &buf[..done])?;
|
||||
Ok(done)
|
||||
} else {
|
||||
op.read_bytes_at(buf_start, &mut buf)?;
|
||||
while done < seg_len {
|
||||
match file.write_at(&buf[done..], file_offset + done as u64) {
|
||||
Ok(0) => break, // no progress: avoid spinning forever
|
||||
Ok(n) => done += n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(done)
|
||||
}
|
||||
}
|
||||
|
||||
// Single-extent path: the whole request lives in `extent`.
|
||||
//
|
||||
// The guest iovecs are handed to `AlignedFile::{read,write}_vectored_at`
|
||||
fn single_extent_io(
|
||||
&self,
|
||||
extent: &VmdkExtent,
|
||||
op: &mut AsyncIoOperation,
|
||||
) -> io::Result<usize> {
|
||||
let file = extent.file.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"VMDK extent is not accessible",
|
||||
)
|
||||
})?;
|
||||
let file_offset = extent.file_base_offset + (op.offset() as u64 - extent.virtual_start);
|
||||
let iovecs = op.iovecs();
|
||||
|
||||
// SAFETY: the iovec buffers are owned by `op` and remain valid for the
|
||||
// duration of this call.
|
||||
unsafe {
|
||||
if op.is_read() {
|
||||
file.read_vectored_at(iovecs, file_offset)
|
||||
} else {
|
||||
file.write_vectored_at(iovecs, file_offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: the request straddles >= 2 extents.
|
||||
//
|
||||
// A single guest request here maps onto several different backing files,
|
||||
// Every segment goes through `segment_io` regardless of
|
||||
// alignment.
|
||||
fn spanning_io(&self, op: &mut AsyncIoOperation) -> io::Result<usize> {
|
||||
let start = op.offset() as u64;
|
||||
let total = op.total_len() as u64;
|
||||
let is_read = op.is_read();
|
||||
|
||||
let mut done: u64 = 0;
|
||||
while done < total {
|
||||
let cur = start + done;
|
||||
let extent = self.extent_at(cur).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent")
|
||||
})?;
|
||||
let extent_end = extent.virtual_start + extent.length;
|
||||
// Bytes handled in this extent before reaching its boundary.
|
||||
let seg_len = cmp::min(total - done, extent_end - cur) as usize;
|
||||
let file = extent.file.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"VMDK extent is not accessible",
|
||||
)
|
||||
})?;
|
||||
let file_offset = extent.file_base_offset + (cur - extent.virtual_start);
|
||||
|
||||
let n = Self::segment_io(file, file_offset, op, done as usize, seg_len, is_read)?;
|
||||
done += n as u64;
|
||||
if n < seg_len {
|
||||
break; // short read/write
|
||||
}
|
||||
}
|
||||
|
||||
Ok(done as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncIo for FlatVmdkSync {
|
||||
fn notifier(&self) -> &EventFd {
|
||||
self.completions.notifier()
|
||||
}
|
||||
|
||||
fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> {
|
||||
let start = op.offset() as u64;
|
||||
let total = op.total_len() as u64;
|
||||
let is_read = op.is_read();
|
||||
|
||||
// Bounds check against the virtual disk size (overflow-safe: `start`
|
||||
// is checked before subtracting it from `size`).
|
||||
if start > self.size || total > self.size - start {
|
||||
let error = io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK request [{start}, {}) exceeds virtual size {}",
|
||||
start + total,
|
||||
self.size
|
||||
),
|
||||
);
|
||||
return Err(if is_read {
|
||||
AsyncIoError::ReadVectored(error)
|
||||
} else {
|
||||
AsyncIoError::WriteVectored(error)
|
||||
});
|
||||
}
|
||||
|
||||
// Reject the request up front if any extent it touches forbids it:
|
||||
// NOACCESS extents reject all I/O.
|
||||
if total != 0
|
||||
&& let Err(error) = self.check_access(start, total, is_read)
|
||||
{
|
||||
return Err(if is_read {
|
||||
AsyncIoError::ReadVectored(error)
|
||||
} else {
|
||||
AsyncIoError::WriteVectored(error)
|
||||
});
|
||||
}
|
||||
|
||||
let result = if total == 0 {
|
||||
Ok(0)
|
||||
} else if let Some(extent) = self.extent_at(start) {
|
||||
if start + total <= extent.virtual_start + extent.length {
|
||||
// Entire request fits in one extent
|
||||
self.single_extent_io(extent, &mut op)
|
||||
} else {
|
||||
// Request crosses an extent boundary
|
||||
self.spanning_io(&mut op)
|
||||
}
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"offset outside any VMDK extent",
|
||||
))
|
||||
};
|
||||
|
||||
let bytes = result.map_err(|e| {
|
||||
if is_read {
|
||||
AsyncIoError::ReadVectored(e)
|
||||
} else {
|
||||
AsyncIoError::WriteVectored(e)
|
||||
}
|
||||
})?;
|
||||
|
||||
self.completions
|
||||
.complete(AsyncIoCompletion::from_operation(op, bytes as i32));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||
// Flush every extent: a single guest flush must durably persist data
|
||||
// that may have been written across multiple extent files.
|
||||
for extent in self.extents.iter() {
|
||||
// Skip NoAccess extents, which have no open file.
|
||||
if let Some(file) = extent.file.as_ref() {
|
||||
file.sync_all().map_err(AsyncIoError::Fsync)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(user_data) = user_data {
|
||||
self.completions
|
||||
.complete(AsyncIoCompletion::new(user_data, 0, None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
|
||||
self.completions.next_completed()
|
||||
}
|
||||
|
||||
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
||||
// Flat VMDK is not sparse-capable (see `SparseCapable` impl), so this
|
||||
// should never be negotiated by the guest.
|
||||
Err(AsyncIoError::PunchHole(io::Error::other(
|
||||
"punch_hole not supported for flat VMDK",
|
||||
)))
|
||||
}
|
||||
|
||||
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
||||
Err(AsyncIoError::WriteZeroes(io::Error::other(
|
||||
"write_zeroes not supported for flat VMDK",
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
//! Flat VMDK extent layout: opens the data extents referenced by the
|
||||
//! descriptor and maps the virtual disk onto them.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io;
|
||||
@@ -19,7 +17,7 @@ use std::sync::Arc;
|
||||
use log::warn;
|
||||
|
||||
use crate::formats::vmdk::descriptor::VmdkDescriptor;
|
||||
use crate::{AlignedFile, DiskTopology};
|
||||
use crate::{AlignedFile, DiskTopology, query_device_size};
|
||||
|
||||
const VMDK_SECTOR_SIZE: u64 = 512;
|
||||
|
||||
@@ -346,18 +344,16 @@ impl FlatVmdk {
|
||||
Arc::clone(&self.extents)
|
||||
}
|
||||
|
||||
/// Host allocation size: the sum of every opened extent file's size.
|
||||
/// `NoAccess` extents contribute 0 to the total.
|
||||
/// Host allocation size: the sum of every opened extent's actually
|
||||
/// allocated storage (`st_blocks * 512` for regular files, device size for
|
||||
/// block devices), so sparse extents are reported correctly. `NoAccess`
|
||||
/// extents (no open file) contribute 0, as does any extent whose size
|
||||
/// cannot be queried.
|
||||
pub fn physical_block_size(&self) -> u64 {
|
||||
self.extents
|
||||
.iter()
|
||||
.map(|extent| {
|
||||
extent
|
||||
.file
|
||||
.as_ref()
|
||||
.and_then(|f| f.metadata().ok())
|
||||
.map_or(0, |m| m.len())
|
||||
})
|
||||
.filter_map(|extent| extent.file.as_ref())
|
||||
.map(|f| query_device_size(f.file()).map_or(0, |(_, physical)| physical))
|
||||
.sum()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -8,6 +8,377 @@
|
||||
//! synchronous, extent-aware I/O.
|
||||
|
||||
mod descriptor;
|
||||
mod engine_sync;
|
||||
mod flat;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
pub use descriptor::{has_descriptor_header, is_flat_vmdk};
|
||||
|
||||
use self::engine_sync::FlatVmdkSync;
|
||||
use self::flat::FlatVmdk;
|
||||
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
||||
use crate::{DiskTopology, disk_file};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VmdkDisk {
|
||||
inner: FlatVmdk,
|
||||
}
|
||||
|
||||
impl VmdkDisk {
|
||||
/// Builds a Flat VMDK disk backend.
|
||||
pub fn new(file: File, path: &Path, direct: bool) -> Result<Self, BlockError> {
|
||||
let inner = FlatVmdk::new(file, path, direct)?;
|
||||
Ok(VmdkDisk { inner })
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::DiskSize for VmdkDisk {
|
||||
fn logical_size(&self) -> BlockResult<u64> {
|
||||
Ok(self.inner.virtual_block_size())
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::PhysicalSize for VmdkDisk {
|
||||
fn physical_size(&self) -> BlockResult<u64> {
|
||||
Ok(self.inner.physical_block_size())
|
||||
}
|
||||
}
|
||||
|
||||
// Expose the descriptor file's fd for advisory image locking.
|
||||
impl disk_file::DiskFd for VmdkDisk {
|
||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
||||
BorrowedDiskFd::new(self.inner.as_raw_fd())
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::Geometry for VmdkDisk {
|
||||
fn topology(&self) -> DiskTopology {
|
||||
self.inner.topology()
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::SparseCapable for VmdkDisk {}
|
||||
|
||||
// Flat VMDK keeps no in-memory format metadata, so no-op.
|
||||
impl disk_file::MetadataSync for VmdkDisk {}
|
||||
|
||||
impl disk_file::Resizable for VmdkDisk {
|
||||
fn resize(&mut self, _size: u64) -> BlockResult<()> {
|
||||
Err(BlockError::new(
|
||||
BlockErrorKind::UnsupportedFeature,
|
||||
DiskFileError::ResizeError(io::Error::other("resize not supported for flat VMDK")),
|
||||
)
|
||||
.with_op(ErrorOp::Resize))
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::DiskFile for VmdkDisk {}
|
||||
|
||||
impl disk_file::AsyncDiskFile for VmdkDisk {
|
||||
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
||||
Ok(Box::new(VmdkDisk {
|
||||
inner: self.inner.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
||||
// VMDK provides a synchronous, extent-aware worker, so the io_uring ring
|
||||
// depth is unused here.
|
||||
let _ = ring_depth;
|
||||
|
||||
Ok(Box::new(FlatVmdkSync::new(
|
||||
self.inner.extents(),
|
||||
self.inner.virtual_block_size(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::disk_file::{AsyncDiskFile, DiskFd, DiskSize, PhysicalSize, Resizable};
|
||||
|
||||
const SECTOR: u64 = 512;
|
||||
|
||||
// Builds a flat VMDK in `dir`: a descriptor plus one backing data file per
|
||||
// extent. When `allocate` is true the extent files are filled with real
|
||||
// blocks (fixed / pre-allocated layout used in practice), otherwise they
|
||||
// are created sparse via `set_len` (declared length but no allocated
|
||||
// blocks). `extents` entries are (filename, access, sectors). Returns the
|
||||
// descriptor path.
|
||||
fn build_flat_vmdk(
|
||||
dir: &Path,
|
||||
create_type: &str,
|
||||
extents: &[(&str, &str, u64)],
|
||||
allocate: bool,
|
||||
) -> PathBuf {
|
||||
let mut desc = String::from("# Disk DescriptorFile\n");
|
||||
desc.push_str("version=1\n");
|
||||
desc.push_str("CID=fffffffe\n");
|
||||
desc.push_str("parentCID=ffffffff\n");
|
||||
desc.push_str(&format!("createType={create_type}\n"));
|
||||
desc.push_str("# Extent description\n");
|
||||
|
||||
for (filename, access, sectors) in extents {
|
||||
let mut data = File::create(dir.join(filename)).unwrap();
|
||||
if allocate {
|
||||
data.write_all(&vec![0u8; (sectors * SECTOR) as usize])
|
||||
.unwrap();
|
||||
} else {
|
||||
data.set_len(sectors * SECTOR).unwrap();
|
||||
}
|
||||
data.sync_all().unwrap();
|
||||
desc.push_str(&format!("{access} {sectors} FLAT \"{filename}\"\n"));
|
||||
}
|
||||
|
||||
desc.push_str("# The Disk Data Base\n");
|
||||
desc.push_str("ddb.adapterType = \"ide\"\n");
|
||||
|
||||
let desc_path = dir.join("disk.vmdk");
|
||||
let mut df = File::create(&desc_path).unwrap();
|
||||
df.write_all(desc.as_bytes()).unwrap();
|
||||
df.sync_all().unwrap();
|
||||
desc_path
|
||||
}
|
||||
|
||||
// Sparse extents.
|
||||
fn write_flat_vmdk(dir: &Path, create_type: &str, extents: &[(&str, &str, u64)]) -> PathBuf {
|
||||
build_flat_vmdk(dir, create_type, extents, false)
|
||||
}
|
||||
|
||||
// Fully pre-allocated extents.
|
||||
fn write_flat_vmdk_allocated(
|
||||
dir: &Path,
|
||||
create_type: &str,
|
||||
extents: &[(&str, &str, u64)],
|
||||
) -> PathBuf {
|
||||
build_flat_vmdk(dir, create_type, extents, true)
|
||||
}
|
||||
|
||||
fn open_descriptor(path: &Path) -> File {
|
||||
File::open(path).unwrap()
|
||||
}
|
||||
|
||||
// Writes a descriptor referencing `extent_lines` verbatim (no backing data
|
||||
// files are created). Used to exercise `FlatVmdk::new`'s per-extent
|
||||
// validation, whose zero-size/overflow checks all run before an extent file
|
||||
// would be opened.
|
||||
fn write_descriptor(dir: &Path, create_type: &str, extent_lines: &[&str]) -> PathBuf {
|
||||
let mut desc = String::from("# Disk DescriptorFile\n");
|
||||
desc.push_str("version=1\n");
|
||||
desc.push_str("CID=fffffffe\n");
|
||||
desc.push_str("parentCID=ffffffff\n");
|
||||
desc.push_str(&format!("createType={create_type}\n"));
|
||||
desc.push_str("# Extent description\n");
|
||||
for line in extent_lines {
|
||||
desc.push_str(line);
|
||||
desc.push('\n');
|
||||
}
|
||||
desc.push_str("# The Disk Data Base\n");
|
||||
desc.push_str("ddb.adapterType = \"ide\"\n");
|
||||
|
||||
let desc_path = dir.join("disk.vmdk");
|
||||
let mut df = File::create(&desc_path).unwrap();
|
||||
df.write_all(desc.as_bytes()).unwrap();
|
||||
df.sync_all().unwrap();
|
||||
desc_path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_and_physical_size_single_extent() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
assert_eq!(disk.logical_size().unwrap(), 2048 * SECTOR);
|
||||
// The extent is created sparse (`set_len`), so no blocks are allocated
|
||||
// and the `st_blocks`-based physical size is 0.
|
||||
assert_eq!(disk.physical_size().unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_size_sums_multiple_extents() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR);
|
||||
// Sparse extents: no blocks are allocated, so physical size is 0.
|
||||
assert_eq!(disk.physical_size().unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physical_size_matches_fully_allocated_extents() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk_allocated(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
// Fully pre-allocated extents: host allocation (st_blocks) equals the
|
||||
// declared logical size.
|
||||
assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR);
|
||||
assert_eq!(disk.physical_size().unwrap(), (2048 + 1024) * SECTOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fd_exposes_descriptor_file() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 64)],
|
||||
);
|
||||
let file = open_descriptor(&path);
|
||||
let expected = file.as_raw_fd();
|
||||
|
||||
let disk = VmdkDisk::new(file, &path, false).unwrap();
|
||||
|
||||
assert_eq!(disk.fd().as_raw_fd(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_is_unsupported() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 64)],
|
||||
);
|
||||
let mut disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
let err = disk.resize(4096).unwrap_err();
|
||||
assert_eq!(err.kind(), BlockErrorKind::UnsupportedFeature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_clone_preserves_size() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
let cloned = disk.try_clone().unwrap();
|
||||
assert_eq!(cloned.logical_size().unwrap(), disk.logical_size().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_async_io_builds_worker() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
// Ring depth is ignored by the synchronous VMDK worker.
|
||||
disk.create_async_io(0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_async_io_supports_multi_extent() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
disk.create_async_io(32).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_sector_extent() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-zero-test").unwrap();
|
||||
let path = write_descriptor(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&["RW 0 FLAT \"disk-flat.vmdk\""],
|
||||
);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_extent_size_overflow() {
|
||||
// size_in_sectors * 512 must fit in a u64.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-size-test").unwrap();
|
||||
let line = format!("RW {} FLAT \"disk-flat.vmdk\"", u64::MAX);
|
||||
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_extent_file_offset_overflow() {
|
||||
// The offset * 512 must fit in a u64.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-offset-test").unwrap();
|
||||
let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {}", u64::MAX);
|
||||
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_extent_file_range_overflow() {
|
||||
// Each of offset*512 and size*512 fits in a u64, but their sum (the last
|
||||
// byte the extent addresses in its backing file) overflows.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-range-test").unwrap();
|
||||
// offset_in_sectors = floor(u64::MAX / 512) => offset bytes = u64::MAX -
|
||||
// 511, size 1 sector (512 bytes) pushes the end one byte past u64::MAX.
|
||||
let offset = u64::MAX / 512;
|
||||
let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {offset}");
|
||||
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_total_virtual_size_overflow() {
|
||||
// Two extents whose individual lengths fit in a u64 but whose running
|
||||
// sum (the total virtual disk size) overflows. size = 2^55 - 1 =>
|
||||
// length = 2^64 - 512, two of them overflow the total.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-total-test").unwrap();
|
||||
let size = (1u64 << 55) - 1;
|
||||
let l1 = format!("NOACCESS {size} FLAT \"s001.vmdk\"");
|
||||
let l2 = format!("NOACCESS {size} FLAT \"s002.vmdk\"");
|
||||
let path = write_descriptor(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[l1.as_str(), l2.as_str()],
|
||||
);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user