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
|
//! Flat VMDK extent layout: opens the data extents referenced by the
|
||||||
//! descriptor and maps the virtual disk onto them.
|
//! descriptor and maps the virtual disk onto them.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::ffi::{CString, OsStr};
|
use std::ffi::{CString, OsStr};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -19,7 +17,7 @@ use std::sync::Arc;
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
use crate::formats::vmdk::descriptor::VmdkDescriptor;
|
use crate::formats::vmdk::descriptor::VmdkDescriptor;
|
||||||
use crate::{AlignedFile, DiskTopology};
|
use crate::{AlignedFile, DiskTopology, query_device_size};
|
||||||
|
|
||||||
const VMDK_SECTOR_SIZE: u64 = 512;
|
const VMDK_SECTOR_SIZE: u64 = 512;
|
||||||
|
|
||||||
@@ -346,18 +344,16 @@ impl FlatVmdk {
|
|||||||
Arc::clone(&self.extents)
|
Arc::clone(&self.extents)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Host allocation size: the sum of every opened extent file's size.
|
/// Host allocation size: the sum of every opened extent's actually
|
||||||
/// `NoAccess` extents contribute 0 to the total.
|
/// 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 {
|
pub fn physical_block_size(&self) -> u64 {
|
||||||
self.extents
|
self.extents
|
||||||
.iter()
|
.iter()
|
||||||
.map(|extent| {
|
.filter_map(|extent| extent.file.as_ref())
|
||||||
extent
|
.map(|f| query_device_size(f.file()).map_or(0, |(_, physical)| physical))
|
||||||
.file
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|f| f.metadata().ok())
|
|
||||||
.map_or(0, |m| m.len())
|
|
||||||
})
|
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
// Copyright © 2026, Microsoft Corporation
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
@@ -8,6 +8,377 @@
|
|||||||
//! synchronous, extent-aware I/O.
|
//! synchronous, extent-aware I/O.
|
||||||
|
|
||||||
mod descriptor;
|
mod descriptor;
|
||||||
|
mod engine_sync;
|
||||||
mod flat;
|
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};
|
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