diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index cf442f6..88850f2 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -1,3 +1,5 @@ +use crate::utils::file_size_utils::FileSize; + /// Name of the cookie that contains session information pub const SESSION_COOKIE_NAME: &str = "X-auth-token"; @@ -47,10 +49,10 @@ pub const DISK_NAME_MIN_LEN: usize = 2; pub const DISK_NAME_MAX_LEN: usize = 10; /// Disk size min (B) -pub const DISK_SIZE_MIN: usize = 100 * 1000 * 1000; +pub const DISK_SIZE_MIN: FileSize = FileSize::from_mb(50); /// Disk size max (B) -pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 1000 * 1000 * 2; +pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000); /// Net nat entry comment max size pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250; diff --git a/virtweb_backend/src/controllers/server_controller.rs b/virtweb_backend/src/controllers/server_controller.rs index 0bba981..004651f 100644 --- a/virtweb_backend/src/controllers/server_controller.rs +++ b/virtweb_backend/src/controllers/server_controller.rs @@ -87,8 +87,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { max: DISK_NAME_MAX_LEN, }, disk_size: LenConstraints { - min: DISK_SIZE_MIN, - max: DISK_SIZE_MAX, + min: DISK_SIZE_MIN.as_bytes(), + max: DISK_SIZE_MAX.as_bytes(), }, net_name_size: LenConstraints { min: 2, max: 50 }, diff --git a/virtweb_backend/src/utils/file_disks_utils.rs b/virtweb_backend/src/utils/file_disks_utils.rs index 80bf47d..411c935 100644 --- a/virtweb_backend/src/utils/file_disks_utils.rs +++ b/virtweb_backend/src/utils/file_disks_utils.rs @@ -1,4 +1,5 @@ use crate::constants; +use crate::utils::file_size_utils::FileSize; use std::os::linux::fs::MetadataExt; use std::path::Path; use std::process::Command; @@ -8,13 +9,15 @@ use std::time::UNIX_EPOCH; enum DisksError { #[error("DiskParseError: {0}")] Parse(&'static str), + #[error("DiskCreateError")] + Create, } #[derive(Debug, serde::Serialize)] #[serde(tag = "format")] pub enum DiskFileFormat { Raw { is_sparse: bool }, - QCow2 { virtual_size: usize }, + QCow2 { virtual_size: FileSize }, CompressedRaw, CompressedQCow2, } @@ -22,7 +25,7 @@ pub enum DiskFileFormat { /// Disk file information #[derive(serde::Serialize)] pub struct DiskFileInfo { - pub file_size: usize, + pub file_size: FileSize, #[serde(flatten)] pub format: DiskFileFormat, pub file_name: String, @@ -64,7 +67,7 @@ impl DiskFileInfo { Ok(Self { name, - file_size: metadata.len() as usize, + file_size: FileSize::from_bytes(metadata.len() as usize), format, file_name: file .file_name() @@ -78,6 +81,50 @@ impl DiskFileInfo { .as_secs(), }) } + + /// Create a new empty disk + pub fn create(file: &Path, format: DiskFileFormat, size: FileSize) -> anyhow::Result<()> { + // Prepare command to create file + let res = match format { + DiskFileFormat::Raw { is_sparse } => { + let mut cmd = Command::new("/usr/bin/dd"); + cmd.arg("if=/dev/zero") + .arg(format!("of={}", file.to_string_lossy())) + .arg("bs=1M"); + + match is_sparse { + false => cmd.arg(format!("count={}", size.as_mb())), + true => cmd.arg(format!("seek={}", size.as_mb())).arg("count=0"), + }; + + cmd.output()? + } + + DiskFileFormat::QCow2 { virtual_size } => { + let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); + cmd.arg("create") + .arg("-f") + .arg("qcow2") + .arg(file) + .arg(format!("{}M", virtual_size.as_mb())); + + cmd.output()? + } + _ => anyhow::bail!("Cannot create disk file image of this format: {format:?}!"), + }; + + // Execute Linux command + if !res.status.success() { + log::error!( + "Failed to create disk! stderr={} stdout={}", + String::from_utf8_lossy(&res.stderr), + String::from_utf8_lossy(&res.stdout) + ); + return Err(DisksError::Create.into()); + } + + Ok(()) + } } #[derive(serde::Deserialize)] @@ -87,7 +134,7 @@ struct QCowInfoOutput { } /// Get QCow2 virtual size -fn qcow_virt_size(path: &Path) -> anyhow::Result { +fn qcow_virt_size(path: &Path) -> anyhow::Result { // Run qemu-img let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); cmd.args([ @@ -110,5 +157,5 @@ fn qcow_virt_size(path: &Path) -> anyhow::Result { // Decode JSON let decoded: QCowInfoOutput = serde_json::from_str(&res_json)?; - Ok(decoded.virtual_size) + Ok(FileSize::from_bytes(decoded.virtual_size)) } diff --git a/virtweb_backend/src/utils/file_size_utils.rs b/virtweb_backend/src/utils/file_size_utils.rs new file mode 100644 index 0000000..0883401 --- /dev/null +++ b/virtweb_backend/src/utils/file_size_utils.rs @@ -0,0 +1,28 @@ +#[derive( + serde::Serialize, serde::Deserialize, Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, +)] +pub struct FileSize(usize); + +impl FileSize { + pub const fn from_bytes(size: usize) -> Self { + Self(size) + } + + pub const fn from_mb(mb: usize) -> Self { + Self(mb * 1000 * 1000) + } + + pub const fn from_gb(gb: usize) -> Self { + Self(gb * 1000 * 1000 * 1000) + } + + /// Get file size as bytes + pub fn as_bytes(&self) -> usize { + self.0 + } + + /// Get file size as megabytes + pub fn as_mb(&self) -> usize { + self.0 / (1000 * 1000) + } +} diff --git a/virtweb_backend/src/utils/mod.rs b/virtweb_backend/src/utils/mod.rs index fbbfb33..92023bb 100644 --- a/virtweb_backend/src/utils/mod.rs +++ b/virtweb_backend/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod exec_utils; pub mod file_disks_utils; +pub mod file_size_utils; pub mod files_utils; pub mod net_utils; pub mod rand_utils; diff --git a/virtweb_backend/src/utils/vm_file_disks_utils.rs b/virtweb_backend/src/utils/vm_file_disks_utils.rs index aede09e..5b10478 100644 --- a/virtweb_backend/src/utils/vm_file_disks_utils.rs +++ b/virtweb_backend/src/utils/vm_file_disks_utils.rs @@ -2,21 +2,19 @@ use crate::app_config::AppConfig; use crate::constants; use crate::libvirt_lib_structures::XMLUuid; use crate::utils::file_disks_utils::{DiskFileFormat, DiskFileInfo}; +use crate::utils::file_size_utils::FileSize; use crate::utils::files_utils; use lazy_regex::regex; use std::path::{Path, PathBuf}; -use std::process::Command; #[derive(thiserror::Error, Debug)] enum VMDisksError { #[error("DiskConfigError: {0}")] Config(&'static str), - #[error("DiskCreateError")] - Create, } /// Type of disk allocation -#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] pub enum VMDiskAllocType { Fixed, Sparse, @@ -38,7 +36,7 @@ pub struct VMFileDisk { /// Disk name pub name: String, /// Disk size, in bytes - pub size: usize, + pub size: FileSize, /// Disk format #[serde(flatten)] pub format: VMDiskFormat, @@ -129,51 +127,20 @@ impl VMFileDisk { return Ok(()); } - // Prepare command to create file - let res = match self.format { - VMDiskFormat::Raw { alloc_type } => { - let mut cmd = Command::new("/usr/bin/dd"); - cmd.arg("if=/dev/zero") - .arg(format!("of={}", file.to_string_lossy())) - .arg("bs=1M"); - - match alloc_type { - VMDiskAllocType::Fixed => cmd.arg(format!("count={}", self.size_mb())), - VMDiskAllocType::Sparse => { - cmd.arg(format!("seek={}", self.size_mb())).arg("count=0") - } - }; - - cmd.output()? - } - - VMDiskFormat::QCow2 => { - let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); - cmd.arg("create") - .arg("-f") - .arg("qcow2") - .arg(file) - .arg(format!("{}M", self.size_mb())); - - cmd.output()? - } - }; - - // Execute Linux command - if !res.status.success() { - log::error!( - "Failed to create disk! stderr={} stdout={}", - String::from_utf8_lossy(&res.stderr), - String::from_utf8_lossy(&res.stdout) - ); - return Err(VMDisksError::Create.into()); - } + // Create disk file + DiskFileInfo::create( + &file, + match self.format { + VMDiskFormat::Raw { alloc_type } => DiskFileFormat::Raw { + is_sparse: alloc_type == VMDiskAllocType::Sparse, + }, + VMDiskFormat::QCow2 => DiskFileFormat::QCow2 { + virtual_size: self.size, + }, + }, + self.size, + )?; Ok(()) } - - /// Get the size of file disk in megabytes - pub fn size_mb(&self) -> usize { - self.size / (1000 * 1000) - } }