diff --git a/virtweb_backend/src/libvirt_rest_structures/vm.rs b/virtweb_backend/src/libvirt_rest_structures/vm.rs index 3222d10..5d1e1ae 100644 --- a/virtweb_backend/src/libvirt_rest_structures/vm.rs +++ b/virtweb_backend/src/libvirt_rest_structures/vm.rs @@ -4,9 +4,9 @@ use crate::libvirt_lib_structures::XMLUuid; use crate::libvirt_lib_structures::domain::*; use crate::libvirt_rest_structures::LibVirtStructError; use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; -use crate::utils::file_disks_utils::{VMDiskFormat, VMFileDisk}; use crate::utils::files_utils; use crate::utils::files_utils::convert_size_unit_to_mb; +use crate::utils::vm_file_disks_utils::{VMDiskFormat, VMFileDisk}; use lazy_regex::regex; use num::Integer; diff --git a/virtweb_backend/src/utils/file_disks_utils.rs b/virtweb_backend/src/utils/file_disks_utils.rs index fa477df..80bf47d 100644 --- a/virtweb_backend/src/utils/file_disks_utils.rs +++ b/virtweb_backend/src/utils/file_disks_utils.rs @@ -1,10 +1,6 @@ -use crate::app_config::AppConfig; use crate::constants; -use crate::libvirt_lib_structures::XMLUuid; -use crate::utils::files_utils; -use lazy_regex::regex; use std::os::linux::fs::MetadataExt; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use std::time::UNIX_EPOCH; @@ -12,173 +8,6 @@ use std::time::UNIX_EPOCH; enum DisksError { #[error("DiskParseError: {0}")] Parse(&'static str), - #[error("DiskConfigError: {0}")] - Config(&'static str), - #[error("DiskCreateError")] - Create, -} - -/// Type of disk allocation -#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum VMDiskAllocType { - Fixed, - Sparse, -} - -/// Disk allocation type -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(tag = "format")] -pub enum VMDiskFormat { - Raw { - /// Type of disk allocation - alloc_type: VMDiskAllocType, - }, - QCow2, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct VMFileDisk { - /// Disk name - pub name: String, - /// Disk size, in bytes - pub size: usize, - /// Disk format - #[serde(flatten)] - pub format: VMDiskFormat, - /// Set this variable to true to delete the disk - pub delete: bool, -} - -impl VMFileDisk { - pub fn load_from_file(path: &str) -> anyhow::Result { - let file = Path::new(path); - - let info = DiskFileInfo::load_file(file)?; - - Ok(Self { - name: info.name, - - // Get only the virtual size of the file - size: match info.format { - DiskFileFormat::Raw { .. } => info.file_size, - DiskFileFormat::QCow2 { virtual_size } => virtual_size, - _ => anyhow::bail!("Unsupported image format: {:?}", info.format), - }, - - format: match info.format { - DiskFileFormat::Raw { is_sparse } => VMDiskFormat::Raw { - alloc_type: match is_sparse { - true => VMDiskAllocType::Sparse, - false => VMDiskAllocType::Fixed, - }, - }, - DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2, - _ => anyhow::bail!("Unsupported image format: {:?}", info.format), - }, - delete: false, - }) - } - - pub fn check_config(&self) -> anyhow::Result<()> { - if constants::DISK_NAME_MIN_LEN > self.name.len() - || constants::DISK_NAME_MAX_LEN < self.name.len() - { - return Err(DisksError::Config("Disk name length is invalid").into()); - } - - if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { - return Err(DisksError::Config("Disk name contains invalid characters!").into()); - } - - // Check disk size - if !(constants::DISK_SIZE_MIN..=constants::DISK_SIZE_MAX).contains(&self.size) { - return Err(DisksError::Config("Disk size is invalid!").into()); - } - - Ok(()) - } - - /// Get disk path - pub fn disk_path(&self, id: XMLUuid) -> PathBuf { - let domain_dir = AppConfig::get().vm_storage_path(id); - let file_name = match self.format { - VMDiskFormat::Raw { .. } => self.name.to_string(), - VMDiskFormat::QCow2 => format!("{}.qcow2", self.name), - }; - domain_dir.join(&file_name) - } - - /// Apply disk configuration - pub fn apply_config(&self, id: XMLUuid) -> anyhow::Result<()> { - self.check_config()?; - - let file = self.disk_path(id); - files_utils::create_directory_if_missing(file.parent().unwrap())?; - - // Delete file if requested - if self.delete { - if !file.exists() { - log::debug!("File {file:?} does not exists, so it was not deleted"); - return Ok(()); - } - - log::info!("Deleting {file:?}"); - std::fs::remove_file(file)?; - return Ok(()); - } - - if file.exists() { - log::debug!("File {file:?} does not exists, so it was not touched"); - 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(DisksError::Create.into()); - } - - Ok(()) - } - - /// Get the size of file disk in megabytes - pub fn size_mb(&self) -> usize { - self.size / (1000 * 1000) - } } #[derive(Debug, serde::Serialize)] @@ -193,12 +22,12 @@ pub enum DiskFileFormat { /// Disk file information #[derive(serde::Serialize)] pub struct DiskFileInfo { - file_size: usize, + pub file_size: usize, #[serde(flatten)] - format: DiskFileFormat, - file_name: String, - name: String, - created: u64, + pub format: DiskFileFormat, + pub file_name: String, + pub name: String, + pub created: u64, } impl DiskFileInfo { diff --git a/virtweb_backend/src/utils/mod.rs b/virtweb_backend/src/utils/mod.rs index 3bdd6bf..fbbfb33 100644 --- a/virtweb_backend/src/utils/mod.rs +++ b/virtweb_backend/src/utils/mod.rs @@ -5,3 +5,4 @@ pub mod net_utils; pub mod rand_utils; pub mod time_utils; pub mod url_utils; +pub mod vm_file_disks_utils; diff --git a/virtweb_backend/src/utils/vm_file_disks_utils.rs b/virtweb_backend/src/utils/vm_file_disks_utils.rs new file mode 100644 index 0000000..aede09e --- /dev/null +++ b/virtweb_backend/src/utils/vm_file_disks_utils.rs @@ -0,0 +1,179 @@ +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::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)] +pub enum VMDiskAllocType { + Fixed, + Sparse, +} + +/// Disk allocation type +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(tag = "format")] +pub enum VMDiskFormat { + Raw { + /// Type of disk allocation + alloc_type: VMDiskAllocType, + }, + QCow2, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct VMFileDisk { + /// Disk name + pub name: String, + /// Disk size, in bytes + pub size: usize, + /// Disk format + #[serde(flatten)] + pub format: VMDiskFormat, + /// Set this variable to true to delete the disk + pub delete: bool, +} + +impl VMFileDisk { + pub fn load_from_file(path: &str) -> anyhow::Result { + let file = Path::new(path); + + let info = DiskFileInfo::load_file(file)?; + + Ok(Self { + name: info.name, + + // Get only the virtual size of the file + size: match info.format { + DiskFileFormat::Raw { .. } => info.file_size, + DiskFileFormat::QCow2 { virtual_size } => virtual_size, + _ => anyhow::bail!("Unsupported image format: {:?}", info.format), + }, + + format: match info.format { + DiskFileFormat::Raw { is_sparse } => VMDiskFormat::Raw { + alloc_type: match is_sparse { + true => VMDiskAllocType::Sparse, + false => VMDiskAllocType::Fixed, + }, + }, + DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2, + _ => anyhow::bail!("Unsupported image format: {:?}", info.format), + }, + delete: false, + }) + } + + pub fn check_config(&self) -> anyhow::Result<()> { + if constants::DISK_NAME_MIN_LEN > self.name.len() + || constants::DISK_NAME_MAX_LEN < self.name.len() + { + return Err(VMDisksError::Config("Disk name length is invalid").into()); + } + + if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { + return Err(VMDisksError::Config("Disk name contains invalid characters!").into()); + } + + // Check disk size + if !(constants::DISK_SIZE_MIN..=constants::DISK_SIZE_MAX).contains(&self.size) { + return Err(VMDisksError::Config("Disk size is invalid!").into()); + } + + Ok(()) + } + + /// Get disk path + pub fn disk_path(&self, id: XMLUuid) -> PathBuf { + let domain_dir = AppConfig::get().vm_storage_path(id); + let file_name = match self.format { + VMDiskFormat::Raw { .. } => self.name.to_string(), + VMDiskFormat::QCow2 => format!("{}.qcow2", self.name), + }; + domain_dir.join(&file_name) + } + + /// Apply disk configuration + pub fn apply_config(&self, id: XMLUuid) -> anyhow::Result<()> { + self.check_config()?; + + let file = self.disk_path(id); + files_utils::create_directory_if_missing(file.parent().unwrap())?; + + // Delete file if requested + if self.delete { + if !file.exists() { + log::debug!("File {file:?} does not exists, so it was not deleted"); + return Ok(()); + } + + log::info!("Deleting {file:?}"); + std::fs::remove_file(file)?; + return Ok(()); + } + + if file.exists() { + log::debug!("File {file:?} does not exists, so it was not touched"); + 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()); + } + + Ok(()) + } + + /// Get the size of file disk in megabytes + pub fn size_mb(&self) -> usize { + self.size / (1000 * 1000) + } +} diff --git a/virtweb_docs/SETUP_DEV.md b/virtweb_docs/SETUP_DEV.md index e621fd6..b587d05 100644 --- a/virtweb_docs/SETUP_DEV.md +++ b/virtweb_docs/SETUP_DEV.md @@ -34,7 +34,7 @@ docker compose up sudo mkdir /var/virtweb sudo chown $USER:$USER /var/virtweb cd virtweb_backend -cargo fmt && cargo clippy && cargo run -- -s /var/virtweb --hypervisor-uri "qemu:///system" +cargo fmt && cargo clippy && cargo run -- -s /var/virtweb --hypervisor-uri "qemu:///system" --website-origin "http://localhost:5173" ``` 7. Run the frontend