Compare commits
	
		
			1 Commits
		
	
	
		
			5cdbb58c03
			...
			3d4750de21
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3d4750de21 | 
| @@ -250,11 +250,6 @@ impl AppConfig { | ||||
|         self.storage_path().join("iso") | ||||
|     } | ||||
|  | ||||
|     /// Get disk images storage directory | ||||
|     pub fn disk_images_storage_path(&self) -> PathBuf { | ||||
|         self.storage_path().join("disk_images") | ||||
|     } | ||||
|  | ||||
|     /// Get VM vnc sockets directory | ||||
|     pub fn vnc_sockets_path(&self) -> PathBuf { | ||||
|         self.storage_path().join("vnc") | ||||
|   | ||||
| @@ -27,13 +27,6 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [ | ||||
| /// ISO max size | ||||
| pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000; | ||||
|  | ||||
| /// Allowed uploaded disk images formats | ||||
| pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 2] = | ||||
|     ["application/x-qemu-disk", "application/gzip"]; | ||||
|  | ||||
| /// Disk image max size | ||||
| pub const DISK_IMAGE_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000 * 1000; | ||||
|  | ||||
| /// Min VM memory size (MB) | ||||
| pub const MIN_VM_MEMORY: usize = 100; | ||||
|  | ||||
| @@ -46,11 +39,11 @@ pub const DISK_NAME_MIN_LEN: usize = 2; | ||||
| /// Disk name max length | ||||
| pub const DISK_NAME_MAX_LEN: usize = 10; | ||||
|  | ||||
| /// Disk size min (B) | ||||
| pub const DISK_SIZE_MIN: usize = 100 * 1000 * 1000; | ||||
| /// Disk size min (MB) | ||||
| pub const DISK_SIZE_MIN: usize = 100; | ||||
|  | ||||
| /// Disk size max (B) | ||||
| pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 1000 * 1000 * 2; | ||||
| /// Disk size max (MB) | ||||
| pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; | ||||
|  | ||||
| /// Net nat entry comment max size | ||||
| pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250; | ||||
|   | ||||
| @@ -1,69 +0,0 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use crate::controllers::HttpResult; | ||||
| use crate::utils::file_disks_utils::DiskFileInfo; | ||||
| use crate::utils::files_utils; | ||||
| use actix_multipart::form::MultipartForm; | ||||
| use actix_multipart::form::tempfile::TempFile; | ||||
| use actix_web::HttpResponse; | ||||
|  | ||||
| #[derive(Debug, MultipartForm)] | ||||
| pub struct UploadDiskImageForm { | ||||
|     #[multipart(rename = "file")] | ||||
|     files: Vec<TempFile>, | ||||
| } | ||||
|  | ||||
| /// Upload disk image file | ||||
| pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>) -> HttpResult { | ||||
|     if form.files.is_empty() { | ||||
|         log::error!("Missing uploaded disk file!"); | ||||
|         return Ok(HttpResponse::BadRequest().json("Missing file!")); | ||||
|     } | ||||
|  | ||||
|     let file = form.files.remove(0); | ||||
|  | ||||
|     // Check uploaded file size | ||||
|     if file.size > constants::DISK_IMAGE_MAX_SIZE { | ||||
|         return Ok(HttpResponse::BadRequest().json("Disk image max size exceeded!")); | ||||
|     } | ||||
|  | ||||
|     // Check file mime type | ||||
|     if let Some(mime_type) = file.content_type { | ||||
|         if !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref()) { | ||||
|             return Ok(HttpResponse::BadRequest().json(format!( | ||||
|                 "Unsupported file type for disk upload: {}", | ||||
|                 mime_type | ||||
|             ))); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Extract and check file name | ||||
|     let Some(file_name) = file.file_name else { | ||||
|         return Ok(HttpResponse::BadRequest().json("Missing file name of uploaded file!")); | ||||
|     }; | ||||
|     if !files_utils::check_file_name(&file_name) { | ||||
|         return Ok(HttpResponse::BadRequest().json("Invalid uploaded file name!")); | ||||
|     } | ||||
|  | ||||
|     // Check if a file with the same name already exists | ||||
|     let dest_path = AppConfig::get().disk_images_storage_path().join(file_name); | ||||
|     if dest_path.is_file() { | ||||
|         return Ok(HttpResponse::Conflict().json("A file with the same name already exists!")); | ||||
|     } | ||||
|  | ||||
|     // Copy the file to the destination | ||||
|     file.file.persist(dest_path)?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json("Successfully uploaded disk image!")) | ||||
| } | ||||
|  | ||||
| /// Get disk images list | ||||
| pub async fn get_list() -> HttpResult { | ||||
|     let mut list = vec![]; | ||||
|     for entry in AppConfig::get().disk_images_storage_path().read_dir()? { | ||||
|         let entry = entry?; | ||||
|         list.push(DiskFileInfo::load_file(&entry.path())?); | ||||
|     } | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(list)) | ||||
| } | ||||
| @@ -7,7 +7,6 @@ use std::fmt::{Display, Formatter}; | ||||
|  | ||||
| pub mod api_tokens_controller; | ||||
| pub mod auth_controller; | ||||
| pub mod disk_images_controller; | ||||
| pub mod groups_controller; | ||||
| pub mod iso_controller; | ||||
| pub mod network_controller; | ||||
|   | ||||
| @@ -16,7 +16,6 @@ struct StaticConfig { | ||||
|     local_auth_enabled: bool, | ||||
|     oidc_auth_enabled: bool, | ||||
|     iso_mimetypes: &'static [&'static str], | ||||
|     disk_images_mimetypes: &'static [&'static str], | ||||
|     net_mac_prefix: &'static str, | ||||
|     builtin_nwfilter_rules: &'static [&'static str], | ||||
|     nwfilter_chains: &'static [&'static str], | ||||
| @@ -38,7 +37,6 @@ struct SLenConstraints { | ||||
| #[derive(serde::Serialize)] | ||||
| struct ServerConstraints { | ||||
|     iso_max_size: usize, | ||||
|     disk_image_max_size: usize, | ||||
|     vnc_token_duration: u64, | ||||
|     vm_name_size: LenConstraints, | ||||
|     vm_title_size: LenConstraints, | ||||
| @@ -65,13 +63,11 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
|         local_auth_enabled: *local_auth, | ||||
|         oidc_auth_enabled: !AppConfig::get().disable_oidc, | ||||
|         iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES, | ||||
|         disk_images_mimetypes: &constants::ALLOWED_DISK_IMAGES_MIME_TYPES, | ||||
|         net_mac_prefix: constants::NET_MAC_ADDR_PREFIX, | ||||
|         builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES, | ||||
|         nwfilter_chains: &constants::NETWORK_CHAINS, | ||||
|         constraints: ServerConstraints { | ||||
|             iso_max_size: constants::ISO_MAX_SIZE, | ||||
|             disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE, | ||||
|  | ||||
|             vnc_token_duration: VNC_TOKEN_LIFETIME, | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ 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::file_disks_utils::{DiskFormat, FileDisk}; | ||||
| use crate::utils::files_utils; | ||||
| use crate::utils::files_utils::convert_size_unit_to_mb; | ||||
| use lazy_regex::regex; | ||||
| @@ -79,7 +79,7 @@ pub struct VMInfo { | ||||
|     /// Attach ISO file(s) | ||||
|     pub iso_files: Vec<String>, | ||||
|     /// File Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest | ||||
|     pub file_disks: Vec<VMFileDisk>, | ||||
|     pub file_disks: Vec<FileDisk>, | ||||
|     /// Network cards | ||||
|     pub networks: Vec<Network>, | ||||
|     /// Add a TPM v2.0 module | ||||
| @@ -289,8 +289,8 @@ impl VMInfo { | ||||
|                 driver: DiskDriverXML { | ||||
|                     name: "qemu".to_string(), | ||||
|                     r#type: match disk.format { | ||||
|                         VMDiskFormat::Raw { .. } => "raw".to_string(), | ||||
|                         VMDiskFormat::QCow2 => "qcow2".to_string(), | ||||
|                         DiskFormat::Raw { .. } => "raw".to_string(), | ||||
|                         DiskFormat::QCow2 => "qcow2".to_string(), | ||||
|                     }, | ||||
|                     cache: "none".to_string(), | ||||
|                 }, | ||||
| @@ -467,7 +467,7 @@ impl VMInfo { | ||||
|                 .disks | ||||
|                 .iter() | ||||
|                 .filter(|d| d.device == "disk") | ||||
|                 .map(|d| VMFileDisk::load_from_file(&d.source.file).unwrap()) | ||||
|                 .map(|d| FileDisk::load_from_file(&d.source.file).unwrap()) | ||||
|                 .collect(), | ||||
|  | ||||
|             networks: domain | ||||
|   | ||||
| @@ -13,7 +13,6 @@ use actix_web::middleware::Logger; | ||||
| use actix_web::web::Data; | ||||
| use actix_web::{App, HttpServer, web}; | ||||
| use light_openid::basic_state_manager::BasicStateManager; | ||||
| use std::cmp::max; | ||||
| use std::time::Duration; | ||||
| use virtweb_backend::actors::libvirt_actor::LibVirtActor; | ||||
| use virtweb_backend::actors::vnc_tokens_actor::VNCTokensManager; | ||||
| @@ -23,9 +22,8 @@ use virtweb_backend::constants::{ | ||||
|     MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, | ||||
| }; | ||||
| use virtweb_backend::controllers::{ | ||||
|     api_tokens_controller, auth_controller, disk_images_controller, groups_controller, | ||||
|     iso_controller, network_controller, nwfilter_controller, server_controller, static_controller, | ||||
|     vm_controller, | ||||
|     api_tokens_controller, auth_controller, groups_controller, iso_controller, network_controller, | ||||
|     nwfilter_controller, server_controller, static_controller, vm_controller, | ||||
| }; | ||||
| use virtweb_backend::libvirt_client::LibVirtClient; | ||||
| use virtweb_backend::middlewares::auth_middleware::AuthChecker; | ||||
| @@ -57,7 +55,6 @@ async fn main() -> std::io::Result<()> { | ||||
|  | ||||
|     log::debug!("Create required directory, if missing"); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap(); | ||||
|     files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap(); | ||||
| @@ -121,10 +118,7 @@ async fn main() -> std::io::Result<()> { | ||||
|             })) | ||||
|             .app_data(conn.clone()) | ||||
|             // Uploaded files | ||||
|             .app_data( | ||||
|                 MultipartFormConfig::default() | ||||
|                     .total_limit(max(constants::DISK_IMAGE_MAX_SIZE, constants::ISO_MAX_SIZE)), | ||||
|             ) | ||||
|             .app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE)) | ||||
|             .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir)) | ||||
|             // Server controller | ||||
|             .route( | ||||
| @@ -335,15 +329,6 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::delete().to(nwfilter_controller::delete), | ||||
|             ) | ||||
|             // Disk images library | ||||
|             .route( | ||||
|                 "/api/disk_images/upload", | ||||
|                 web::post().to(disk_images_controller::upload), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/disk_images/list", | ||||
|                 web::get().to(disk_images_controller::get_list), | ||||
|             ) | ||||
|             // API tokens controller | ||||
|             .route( | ||||
|                 "/api/token/create", | ||||
|   | ||||
| @@ -6,7 +6,6 @@ use lazy_regex::regex; | ||||
| use std::os::linux::fs::MetadataExt; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::process::Command; | ||||
| use std::time::UNIX_EPOCH; | ||||
|  | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum DisksError { | ||||
| @@ -20,7 +19,7 @@ enum DisksError { | ||||
|  | ||||
| /// Type of disk allocation | ||||
| #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] | ||||
| pub enum VMDiskAllocType { | ||||
| pub enum DiskAllocType { | ||||
|     Fixed, | ||||
|     Sparse, | ||||
| } | ||||
| @@ -28,53 +27,60 @@ pub enum VMDiskAllocType { | ||||
| /// Disk allocation type | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(tag = "format")] | ||||
| pub enum VMDiskFormat { | ||||
| pub enum DiskFormat { | ||||
|     Raw { | ||||
|         /// Type of disk allocation | ||||
|         alloc_type: VMDiskAllocType, | ||||
|         alloc_type: DiskAllocType, | ||||
|     }, | ||||
|     QCow2, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct VMFileDisk { | ||||
| pub struct FileDisk { | ||||
|     /// Disk name | ||||
|     pub name: String, | ||||
|     /// Disk size, in bytes | ||||
|     /// Disk size, in megabytes | ||||
|     pub size: usize, | ||||
|     /// Disk format | ||||
|     #[serde(flatten)] | ||||
|     pub format: VMDiskFormat, | ||||
|     pub format: DiskFormat, | ||||
|     /// Set this variable to true to delete the disk | ||||
|     pub delete: bool, | ||||
| } | ||||
|  | ||||
| impl VMFileDisk { | ||||
| impl FileDisk { | ||||
|     pub fn load_from_file(path: &str) -> anyhow::Result<Self> { | ||||
|         let file = Path::new(path); | ||||
|  | ||||
|         let info = DiskFileInfo::load_file(file)?; | ||||
|         if !file.is_file() { | ||||
|             return Err(DisksError::Parse("Path is not a file!").into()); | ||||
|         } | ||||
|  | ||||
|         let metadata = file.metadata()?; | ||||
|         let name = file.file_stem().and_then(|s| s.to_str()).unwrap_or("disk"); | ||||
|         let ext = file.extension().and_then(|s| s.to_str()).unwrap_or("raw"); | ||||
|  | ||||
|         // Approximate raw file estimation | ||||
|         let is_raw_sparse = metadata.len() / 512 >= metadata.st_blocks(); | ||||
|  | ||||
|         let format = match ext { | ||||
|             "qcow2" => DiskFormat::QCow2, | ||||
|             "raw" => DiskFormat::Raw { | ||||
|                 alloc_type: match is_raw_sparse { | ||||
|                     true => DiskAllocType::Sparse, | ||||
|                     false => DiskAllocType::Fixed, | ||||
|                 }, | ||||
|             }, | ||||
|             _ => anyhow::bail!("Unsupported disk extension: {ext}!"), | ||||
|         }; | ||||
|  | ||||
|         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), | ||||
|             name: name.to_string(), | ||||
|             size: match format { | ||||
|                 DiskFormat::Raw { .. } => metadata.len() as usize / (1000 * 1000), | ||||
|                 DiskFormat::QCow2 => qcow_virt_size(path)? / (1000 * 1000), | ||||
|             }, | ||||
|             format, | ||||
|             delete: false, | ||||
|         }) | ||||
|     } | ||||
| @@ -102,8 +108,8 @@ impl VMFileDisk { | ||||
|     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), | ||||
|             DiskFormat::Raw { .. } => self.name.to_string(), | ||||
|             DiskFormat::QCow2 => format!("{}.qcow2", self.name), | ||||
|         }; | ||||
|         domain_dir.join(&file_name) | ||||
|     } | ||||
| @@ -134,29 +140,27 @@ impl VMFileDisk { | ||||
|  | ||||
|         // Prepare command to create file | ||||
|         let res = match self.format { | ||||
|             VMDiskFormat::Raw { alloc_type } => { | ||||
|             DiskFormat::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") | ||||
|                     } | ||||
|                     DiskAllocType::Fixed => cmd.arg(format!("count={}", self.size)), | ||||
|                     DiskAllocType::Sparse => cmd.arg(format!("seek={}", self.size)).arg("count=0"), | ||||
|                 }; | ||||
|  | ||||
|                 cmd.output()? | ||||
|             } | ||||
|  | ||||
|             VMDiskFormat::QCow2 => { | ||||
|             DiskFormat::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())); | ||||
|                     .arg(format!("{}M", self.size)); | ||||
|  | ||||
|                 cmd.output()? | ||||
|             } | ||||
| @@ -174,81 +178,6 @@ impl VMFileDisk { | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Get the size of file disk in megabytes | ||||
|     pub fn size_mb(&self) -> usize { | ||||
|         self.size / (1000 * 1000) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, serde::Serialize)] | ||||
| #[serde(tag = "format")] | ||||
| pub enum DiskFileFormat { | ||||
|     Raw { is_sparse: bool }, | ||||
|     QCow2 { virtual_size: usize }, | ||||
|     CompressedRaw, | ||||
|     CompressedQCow2, | ||||
| } | ||||
|  | ||||
| /// Disk file information | ||||
| #[derive(serde::Serialize)] | ||||
| pub struct DiskFileInfo { | ||||
|     file_size: usize, | ||||
|     #[serde(flatten)] | ||||
|     format: DiskFileFormat, | ||||
|     file_name: String, | ||||
|     name: String, | ||||
|     created: u64, | ||||
| } | ||||
|  | ||||
| impl DiskFileInfo { | ||||
|     /// Get disk image file information | ||||
|     pub fn load_file(file: &Path) -> anyhow::Result<Self> { | ||||
|         if !file.is_file() { | ||||
|             return Err(DisksError::Parse("Path is not a file!").into()); | ||||
|         } | ||||
|  | ||||
|         // Get file metadata | ||||
|         let metadata = file.metadata()?; | ||||
|         let mut name = file | ||||
|             .file_stem() | ||||
|             .and_then(|s| s.to_str()) | ||||
|             .unwrap_or("disk") | ||||
|             .to_string(); | ||||
|         let ext = file.extension().and_then(|s| s.to_str()).unwrap_or("raw"); | ||||
|  | ||||
|         // Determine file format | ||||
|         let format = match ext { | ||||
|             "qcow2" => DiskFileFormat::QCow2 { | ||||
|                 virtual_size: qcow_virt_size(file)?, | ||||
|             }, | ||||
|             "raw" => DiskFileFormat::Raw { | ||||
|                 is_sparse: metadata.len() / 512 >= metadata.st_blocks(), | ||||
|             }, | ||||
|             "gz" if name.ends_with(".qcow2") => { | ||||
|                 name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string(); | ||||
|                 DiskFileFormat::CompressedQCow2 | ||||
|             } | ||||
|             "gz" => DiskFileFormat::CompressedRaw, | ||||
|             _ => anyhow::bail!("Unsupported disk extension: {ext}!"), | ||||
|         }; | ||||
|  | ||||
|         Ok(Self { | ||||
|             name, | ||||
|             file_size: metadata.len() as usize, | ||||
|             format, | ||||
|             file_name: file | ||||
|                 .file_name() | ||||
|                 .and_then(|s| s.to_str()) | ||||
|                 .unwrap_or("") | ||||
|                 .to_string(), | ||||
|             created: metadata | ||||
|                 .created()? | ||||
|                 .duration_since(UNIX_EPOCH) | ||||
|                 .unwrap() | ||||
|                 .as_secs(), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| @@ -258,16 +187,10 @@ struct QCowInfoOutput { | ||||
| } | ||||
|  | ||||
| /// Get QCow2 virtual size | ||||
| fn qcow_virt_size(path: &Path) -> anyhow::Result<usize> { | ||||
| fn qcow_virt_size(path: &str) -> anyhow::Result<usize> { | ||||
|     // Run qemu-img | ||||
|     let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); | ||||
|     cmd.args([ | ||||
|         "info", | ||||
|         path.to_str().unwrap_or(""), | ||||
|         "--output", | ||||
|         "json", | ||||
|         "--force-share", | ||||
|     ]); | ||||
|     cmd.args(["info", path, "--output", "json", "--force-share"]); | ||||
|     let output = cmd.output()?; | ||||
|     if !output.status.success() { | ||||
|         anyhow::bail!( | ||||
|   | ||||
| @@ -38,7 +38,6 @@ import { LoginRoute } from "./routes/auth/LoginRoute"; | ||||
| import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; | ||||
| import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; | ||||
| import { BaseLoginPage } from "./widgets/BaseLoginPage"; | ||||
| import { DiskImagesRoute } from "./routes/DiskImagesRoute"; | ||||
|  | ||||
| interface AuthContext { | ||||
|   signedIn: boolean; | ||||
| @@ -64,8 +63,6 @@ export function App() { | ||||
|         <Route path="*" element={<BaseAuthenticatedPage />}> | ||||
|           <Route path="" element={<HomeRoute />} /> | ||||
|  | ||||
|           <Route path="disk_images" element={<DiskImagesRoute />} /> | ||||
|  | ||||
|           <Route path="iso" element={<IsoFilesRoute />} /> | ||||
|  | ||||
|           <Route path="vms" element={<VMListRoute />} /> | ||||
|   | ||||
| @@ -1,45 +0,0 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
|  | ||||
| export type DiskImage = { | ||||
|   file_size: number; | ||||
|   file_name: string; | ||||
|   name: string; | ||||
|   created: number; | ||||
| } & ( | ||||
|   | { format: "Raw"; is_sparse: boolean } | ||||
|   | { format: "QCow2"; virtual_size: number } | ||||
|   | { format: "CompressedQCow2" } | ||||
|   | { format: "CompressedRaw" } | ||||
| ); | ||||
|  | ||||
| export class DiskImageApi { | ||||
|   /** | ||||
|    * Upload a new disk image file to the server | ||||
|    */ | ||||
|   static async Upload( | ||||
|     file: File, | ||||
|     progress: (progress: number) => void | ||||
|   ): Promise<void> { | ||||
|     const fd = new FormData(); | ||||
|     fd.append("file", file); | ||||
|  | ||||
|     await APIClient.exec({ | ||||
|       method: "POST", | ||||
|       uri: "/disk_images/upload", | ||||
|       formData: fd, | ||||
|       upProgress: progress, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the list of disk images | ||||
|    */ | ||||
|   static async GetList(): Promise<DiskImage[]> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/disk_images/list", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
| } | ||||
| @@ -5,7 +5,6 @@ export interface ServerConfig { | ||||
|   local_auth_enabled: boolean; | ||||
|   oidc_auth_enabled: boolean; | ||||
|   iso_mimetypes: string[]; | ||||
|   disk_images_mimetypes: string[]; | ||||
|   net_mac_prefix: string; | ||||
|   builtin_nwfilter_rules: string[]; | ||||
|   nwfilter_chains: string[]; | ||||
| @@ -14,7 +13,6 @@ export interface ServerConfig { | ||||
|  | ||||
| export interface ServerConstraints { | ||||
|   iso_max_size: number; | ||||
|   disk_image_max_size: number; | ||||
|   vnc_token_duration: number; | ||||
|   vm_name_size: LenConstraint; | ||||
|   vm_title_size: LenConstraint; | ||||
|   | ||||
| @@ -1,152 +0,0 @@ | ||||
| import RefreshIcon from "@mui/icons-material/Refresh"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   LinearProgress, | ||||
|   Tooltip, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { DiskImage, DiskImageApi } from "../api/DiskImageApi"; | ||||
| import { ServerApi } from "../api/ServerApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { FileInput } from "../widgets/forms/FileInput"; | ||||
| import { VirtWebPaper } from "../widgets/VirtWebPaper"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
|  | ||||
| export function DiskImagesRoute(): React.ReactElement { | ||||
|   const [list, setList] = React.useState<DiskImage[] | undefined>(); | ||||
|  | ||||
|   const loadKey = React.useRef(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await DiskImageApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   const reload = () => { | ||||
|     loadKey.current += 1; | ||||
|     setList(undefined); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer label="Disk images"> | ||||
|       <AsyncWidget | ||||
|         loadKey={loadKey.current} | ||||
|         errMsg="Failed to load disk images list!" | ||||
|         load={load} | ||||
|         ready={list !== undefined} | ||||
|         build={() => ( | ||||
|           <VirtWebRouteContainer | ||||
|             label="Disk images management" | ||||
|             actions={ | ||||
|               <span> | ||||
|                 <Tooltip title="Refresh Disk images list"> | ||||
|                   <IconButton onClick={reload}> | ||||
|                     <RefreshIcon /> | ||||
|                   </IconButton> | ||||
|                 </Tooltip> | ||||
|               </span> | ||||
|             } | ||||
|           > | ||||
|             <UploadDiskImageCard onFileUploaded={reload} /> | ||||
|             <DiskImageList list={list!} onReload={reload} /> | ||||
|           </VirtWebRouteContainer> | ||||
|         )} | ||||
|       /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function UploadDiskImageCard(p: { | ||||
|   onFileUploaded: () => void; | ||||
| }): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const [value, setValue] = React.useState<File | null>(null); | ||||
|   const [uploadProgress, setUploadProgress] = React.useState<number | null>( | ||||
|     null | ||||
|   ); | ||||
|  | ||||
|   const handleChange = (newValue: File | null) => { | ||||
|     if ( | ||||
|       newValue && | ||||
|       newValue.size > ServerApi.Config.constraints.disk_image_max_size | ||||
|     ) { | ||||
|       alert( | ||||
|         `The file is too big (max size allowed: ${filesize( | ||||
|           ServerApi.Config.constraints.disk_image_max_size | ||||
|         )}` | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       newValue && | ||||
|       newValue.type.length > 0 && | ||||
|       !ServerApi.Config.disk_images_mimetypes.includes(newValue.type) | ||||
|     ) { | ||||
|       alert(`Selected file mimetype is not allowed! (${newValue.type})`); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setValue(newValue); | ||||
|   }; | ||||
|  | ||||
|   const upload = async () => { | ||||
|     try { | ||||
|       setUploadProgress(0); | ||||
|       await DiskImageApi.Upload(value!, setUploadProgress); | ||||
|  | ||||
|       setValue(null); | ||||
|       snackbar("The file was successfully uploaded!"); | ||||
|  | ||||
|       p.onFileUploaded(); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       await alert(`Failed to perform file upload! ${e}`); | ||||
|     } | ||||
|  | ||||
|     setUploadProgress(null); | ||||
|   }; | ||||
|  | ||||
|   if (uploadProgress !== null) { | ||||
|     return ( | ||||
|       <VirtWebPaper label="File upload" noHorizontalMargin> | ||||
|         <Typography variant="body1"> | ||||
|           Upload in progress ({Math.floor(uploadProgress * 100)}%)... | ||||
|         </Typography> | ||||
|         <LinearProgress variant="determinate" value={uploadProgress * 100} /> | ||||
|       </VirtWebPaper> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebPaper label="Disk image upload" noHorizontalMargin> | ||||
|       <div style={{ display: "flex", alignItems: "center" }}> | ||||
|         <FileInput | ||||
|           value={value} | ||||
|           onChange={handleChange} | ||||
|           style={{ flex: 1 }} | ||||
|           slotProps={{ | ||||
|             htmlInput: { | ||||
|               accept: ServerApi.Config.disk_images_mimetypes.join(","), | ||||
|             }, | ||||
|           }} | ||||
|         /> | ||||
|  | ||||
|         {value && <Button onClick={upload}>Upload</Button>} | ||||
|       </div> | ||||
|     </VirtWebPaper> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function DiskImageList(p: { | ||||
|   list: DiskImage[]; | ||||
|   onReload: () => void; | ||||
| }): React.ReactElement { | ||||
|   return <>todo</>; | ||||
| } | ||||
| @@ -3,15 +3,7 @@ import { RouterLink } from "../widgets/RouterLink"; | ||||
|  | ||||
| export function NotFoundRoute(): React.ReactElement { | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ | ||||
|         textAlign: "center", | ||||
|         flex: 1, | ||||
|         justifyContent: "center", | ||||
|         display: "flex", | ||||
|         flexDirection: "column", | ||||
|       }} | ||||
|     > | ||||
|     <div style={{ textAlign: "center" }}> | ||||
|       <h1>404 Not found</h1> | ||||
|       <p>The page you requested was not found!</p> | ||||
|       <RouterLink to="/"> | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { | ||||
|   mdiApi, | ||||
|   mdiBoxShadow, | ||||
|   mdiDisc, | ||||
|   mdiHarddisk, | ||||
|   mdiHome, | ||||
|   mdiInformation, | ||||
|   mdiLan, | ||||
| @@ -67,11 +66,6 @@ export function BaseAuthenticatedPage(): React.ReactElement { | ||||
|             uri="/nwfilter" | ||||
|             icon={<Icon path={mdiSecurityNetwork} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="Disk images" | ||||
|             uri="/disk_images" | ||||
|             icon={<Icon path={mdiHarddisk} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="ISO files" | ||||
|             uri="/iso" | ||||
|   | ||||
| @@ -35,7 +35,7 @@ export function FileInput( | ||||
|               <InputAdornment position="start"> | ||||
|                 <AttachFileIcon /> | ||||
|                    | ||||
|                 {p.value ? p.value.name : "Select a file"} | ||||
|                 {p.value ? p.value.name : "Insert a file"} | ||||
|               </InputAdornment> | ||||
|             </> | ||||
|           ), | ||||
|   | ||||
| @@ -27,7 +27,7 @@ export function VMDisksList(p: { | ||||
|   const addNewDisk = () => { | ||||
|     p.vm.file_disks.push({ | ||||
|       format: "QCow2", | ||||
|       size: 10000 * 1000 * 1000, | ||||
|       size: 10000, | ||||
|       delete: false, | ||||
|       name: `disk${p.vm.file_disks.length}`, | ||||
|       new: true, | ||||
| @@ -125,9 +125,9 @@ function DiskInfo(p: { | ||||
|               )} | ||||
|             </> | ||||
|           } | ||||
|           secondary={`${filesize(p.disk.size)} - ${p.disk.format}${ | ||||
|             p.disk.format == "Raw" ? " - " + p.disk.alloc_type : "" | ||||
|           }`} | ||||
|           secondary={`${filesize(p.disk.size * 1000 * 1000)} - ${ | ||||
|             p.disk.format | ||||
|           }${p.disk.format == "Raw" ? " - " + p.disk.alloc_type : ""}`} | ||||
|         /> | ||||
|       </ListItem> | ||||
|     ); | ||||
| @@ -153,16 +153,11 @@ function DiskInfo(p: { | ||||
|  | ||||
|       <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()} | ||||
|         label="Disk size (MB)" | ||||
|         size={ServerApi.Config.constraints.disk_size} | ||||
|         value={p.disk.size.toString()} | ||||
|         onValueChange={(v) => { | ||||
|           p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000; | ||||
|           p.disk.size = Number(v ?? "0"); | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|         type="number" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user