block: qcow: Add resize() to QcowMetadata

Add resize() and grow_l1_table() so the metadata layer can grow
the virtual disk size. Only grow is supported.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski
2026-02-17 22:59:21 +01:00
committed by Rob Bradford
parent 9d686b0866
commit a4a5b19f64

View File

@@ -26,8 +26,9 @@ use libc::{EINVAL, EIO};
use super::qcow_raw_file::QcowRawFile;
use super::refcount::RefCount;
use super::util::{
l2_entry_compressed_cluster_layout, l2_entry_is_compressed, l2_entry_is_empty,
l2_entry_is_zero, l2_entry_make_std, l2_entry_make_zero, l2_entry_std_cluster_addr,
div_round_up_u64, l1_entry_make, l2_entry_compressed_cluster_layout, l2_entry_is_compressed,
l2_entry_is_empty, l2_entry_is_zero, l2_entry_make_std, l2_entry_make_zero,
l2_entry_std_cluster_addr,
};
use super::vec_cache::{CacheMap, Cacheable, VecCache};
use super::{QcowHeader, refcount};
@@ -237,6 +238,16 @@ impl QcowMetadata {
Ok(())
}
/// Resizes the QCOW2 image to the given new size. Only grow is
/// supported, shrink would require walking all L2 tables to reclaim
/// clusters beyond the new size and risks data loss.
///
/// Returns an error if the new size is smaller than the current size.
pub fn resize(&self, new_size: u64) -> io::Result<()> {
let mut inner = self.inner.write().unwrap();
inner.resize(new_size)
}
/// Deallocates a range of bytes. Full clusters are deallocated via metadata.
/// Partial clusters need the caller to write zeros. This method returns a
/// list of actions the caller should take.
@@ -658,6 +669,110 @@ impl QcowState {
Ok(())
}
/// Resizes the image to the given new size. Only grow is supported,
/// shrink would require walking all L2 tables to reclaim clusters
/// beyond the new size and risks data loss.
fn resize(&mut self, new_size: u64) -> io::Result<()> {
let current_size = self.header.size;
if new_size == current_size {
return Ok(());
}
if new_size < current_size {
return Err(io::Error::other("shrinking QCOW2 images is not supported"));
}
let cluster_size = self.raw_file.cluster_size();
let entries_per_cluster = cluster_size / size_of::<u64>() as u64;
let new_clusters = div_round_up_u64(new_size, cluster_size);
let needed_l1_entries = div_round_up_u64(new_clusters, entries_per_cluster) as u32;
if needed_l1_entries > self.header.l1_size {
self.grow_l1_table(needed_l1_entries)?;
}
self.header.size = new_size;
self.raw_file.file_mut().rewind()?;
self.header
.write_to(self.raw_file.file_mut())
.map_err(|e| io::Error::other(format!("failed to write header during resize: {e}")))?;
self.raw_file.file_mut().sync_all()?;
Ok(())
}
/// Grows the L1 table to accommodate at least the requested number of entries.
fn grow_l1_table(&mut self, new_l1_size: u32) -> io::Result<()> {
let old_l1_size = self.header.l1_size;
let old_l1_offset = self.header.l1_table_offset;
let cluster_size = self.raw_file.cluster_size();
let new_l1_bytes = new_l1_size as u64 * size_of::<u64>() as u64;
let new_l1_clusters = div_round_up_u64(new_l1_bytes, cluster_size);
// Allocate contiguous clusters at file end for new L1 table
let file_size = self.raw_file.file_mut().seek(io::SeekFrom::End(0))?;
let new_l1_offset = self.raw_file.cluster_address(file_size + cluster_size - 1);
let new_file_end = new_l1_offset + new_l1_clusters * cluster_size;
self.raw_file.file_mut().set_len(new_file_end)?;
// Set refcounts for the contiguous range
for i in 0..new_l1_clusters {
self.set_cluster_refcount_track_freed(new_l1_offset + i * cluster_size, 1)?;
}
let mut new_l1_data = vec![0u64; new_l1_size as usize];
let old_entries = self.l1_table.get_values();
new_l1_data[..old_entries.len()].copy_from_slice(old_entries);
for l2_addr in new_l1_data.iter_mut() {
if *l2_addr != 0 {
let refcount = self
.refcounts
.get_cluster_refcount(&mut self.raw_file, *l2_addr)
.map_err(|e| {
io::Error::other(format!("failed to get refcount during resize: {e}"))
})?;
*l2_addr = l1_entry_make(*l2_addr, refcount == 1);
}
}
// Write the new L1 table to disk
self.raw_file
.write_pointer_table_direct(new_l1_offset, new_l1_data.iter())?;
self.raw_file.file_mut().sync_all()?;
self.header.l1_size = new_l1_size;
self.header.l1_table_offset = new_l1_offset;
self.raw_file.file_mut().rewind()?;
self.header
.write_to(self.raw_file.file_mut())
.map_err(|e| io::Error::other(format!("failed to write header during resize: {e}")))?;
self.raw_file.file_mut().sync_all()?;
// Free old L1 table clusters
let old_l1_bytes = old_l1_size as u64 * size_of::<u64>() as u64;
let old_l1_clusters = div_round_up_u64(old_l1_bytes, cluster_size);
for i in 0..old_l1_clusters {
let cluster_addr = old_l1_offset + i * cluster_size;
// Best effort: the old L1 clusters are no longer reachable,
// so a refcount update failure just leaks space.
let _ = self.set_cluster_refcount(cluster_addr, 0);
}
// Update L1 table cache
self.l1_table.extend(new_l1_size as usize);
Ok(())
}
/// Deallocates a cluster at the given guest address.
///
/// If sparse is true, fully deallocates and returns the host offset if
@@ -804,8 +919,6 @@ impl QcowState {
/// Flushes all dirty metadata to disk.
pub(super) fn sync_caches(&mut self) -> io::Result<()> {
use super::l1_entry_make;
// Write out all dirty L2 tables.
for (l1_index, l2_table) in self.l2_cache.iter_mut().filter(|(_k, v)| v.dirty()) {
let addr = self.l1_table[*l1_index];