Can resize disk image when adding a new disk image to a VM
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -394,6 +394,40 @@ impl DiskFileInfo {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get disk virtual size, if available
|
||||||
|
pub fn virtual_size(&self) -> Option<FileSize> {
|
||||||
|
match self.format {
|
||||||
|
DiskFileFormat::Raw { .. } => Some(self.file_size),
|
||||||
|
DiskFileFormat::QCow2 { virtual_size } => Some(virtual_size),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize disk
|
||||||
|
pub fn resize(&self, new_size: FileSize) -> anyhow::Result<()> {
|
||||||
|
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
|
||||||
|
cmd.arg("resize")
|
||||||
|
.arg("-f")
|
||||||
|
.arg(match self.format {
|
||||||
|
DiskFileFormat::QCow2 { .. } => "qcow2",
|
||||||
|
DiskFileFormat::Raw { .. } => "raw",
|
||||||
|
f => anyhow::bail!("Unsupported disk format for resize: {f:?}"),
|
||||||
|
})
|
||||||
|
.arg(&self.file_path)
|
||||||
|
.arg(new_size.as_bytes().to_string());
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"{} info failed, status: {}, stderr: {}",
|
||||||
|
constants::PROGRAM_QEMU_IMAGE,
|
||||||
|
output.status,
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
|
@ -44,6 +44,9 @@ pub struct VMFileDisk {
|
|||||||
/// When creating a new disk, specify the disk image template to use
|
/// When creating a new disk, specify the disk image template to use
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub from_image: Option<String>,
|
pub from_image: Option<String>,
|
||||||
|
/// Set this variable to true to resize disk image
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub resize: Option<bool>,
|
||||||
/// Set this variable to true to delete the disk
|
/// Set this variable to true to delete the disk
|
||||||
pub delete: bool,
|
pub delete: bool,
|
||||||
}
|
}
|
||||||
@ -78,6 +81,7 @@ impl VMFileDisk {
|
|||||||
|
|
||||||
delete: false,
|
delete: false,
|
||||||
from_image: None,
|
from_image: None,
|
||||||
|
resize: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,28 +148,40 @@ impl VMFileDisk {
|
|||||||
|
|
||||||
if file.exists() {
|
if file.exists() {
|
||||||
log::debug!("File {file:?} does not exists, so it was not touched");
|
log::debug!("File {file:?} does not exists, so it was not touched");
|
||||||
return Ok(());
|
}
|
||||||
|
// Create disk if required
|
||||||
|
else {
|
||||||
|
// Determine file format
|
||||||
|
let format = match self.format {
|
||||||
|
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
|
||||||
|
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
||||||
|
virtual_size: self.size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create / Restore disk file
|
||||||
|
match &self.from_image {
|
||||||
|
// Create disk file
|
||||||
|
None => {
|
||||||
|
DiskFileInfo::create(&file, format, self.size)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore disk image template
|
||||||
|
Some(disk_img) => {
|
||||||
|
let src_file =
|
||||||
|
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
|
||||||
|
src_file.convert(&file, format)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let format = match self.format {
|
// Resize disk file if requested
|
||||||
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
|
if self.resize == Some(true) {
|
||||||
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
let disk = DiskFileInfo::load_file(&file)?;
|
||||||
virtual_size: self.size,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create / Restore disk file
|
// Can only increase disk size
|
||||||
match &self.from_image {
|
if let Err(e) = disk.resize(self.size) {
|
||||||
// Create disk file
|
log::error!("Failed to resize disk file {}: {e:?}", self.name);
|
||||||
None => {
|
|
||||||
DiskFileInfo::create(&file, format, self.size)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore disk image template
|
|
||||||
Some(disk_img) => {
|
|
||||||
let src_file =
|
|
||||||
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
|
|
||||||
src_file.convert(&file, format)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,9 @@ export interface BaseFileVMDisk {
|
|||||||
// For new disk only
|
// For new disk only
|
||||||
from_image?: string;
|
from_image?: string;
|
||||||
|
|
||||||
|
// Resize disk image after clone
|
||||||
|
resize?: boolean;
|
||||||
|
|
||||||
// application attributes
|
// application attributes
|
||||||
new?: boolean;
|
new?: boolean;
|
||||||
deleteType?: "keepfile" | "deletefile";
|
deleteType?: "keepfile" | "deletefile";
|
||||||
|
@ -218,24 +218,40 @@ function DiskInfo(p: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextInput
|
{/* Resize disk image */}
|
||||||
editable={true}
|
{!!p.disk.from_image && (
|
||||||
label="Disk size (GB)"
|
<CheckboxInput
|
||||||
size={{
|
editable
|
||||||
min:
|
checked={p.disk.resize}
|
||||||
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
|
label="Resize disk file"
|
||||||
max:
|
onValueChange={(v) => {
|
||||||
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
|
p.disk.resize = v;
|
||||||
}}
|
p.onChange?.();
|
||||||
value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
|
}}
|
||||||
onValueChange={(v) => {
|
/>
|
||||||
p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
|
)}
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
type="number"
|
|
||||||
disabled={!!p.disk.from_image}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
{/* Disk size */}
|
||||||
|
{(!p.disk.from_image || p.disk.resize === true) && (
|
||||||
|
<TextInput
|
||||||
|
editable={true}
|
||||||
|
label="Disk size (GB)"
|
||||||
|
size={{
|
||||||
|
min:
|
||||||
|
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
|
||||||
|
max:
|
||||||
|
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
|
||||||
|
}}
|
||||||
|
value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Disk image selection */}
|
||||||
<DiskImageSelect
|
<DiskImageSelect
|
||||||
label="Use disk image as template"
|
label="Use disk image as template"
|
||||||
list={p.diskImagesList}
|
list={p.diskImagesList}
|
||||||
|
Reference in New Issue
Block a user