Compare commits
	
		
			5 Commits
		
	
	
		
			47fa43dbbb
			...
			a314e6b41f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a314e6b41f | |||
| 8a7712ec42 | |||
| 9609cfb33a | |||
| 1fe7c60f36 | |||
| f1339f0711 | 
							
								
								
									
										4
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -3413,9 +3413,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "tokio" | ||||
| version = "1.45.0" | ||||
| version = "1.45.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" | ||||
| checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" | ||||
| dependencies = [ | ||||
|  "backtrace", | ||||
|  "bytes", | ||||
|   | ||||
| @@ -36,7 +36,7 @@ lazy-regex = "3.4.1" | ||||
| thiserror = "2.0.12" | ||||
| image = "0.25.6" | ||||
| rand = "0.9.1" | ||||
| tokio = { version = "1.45.0", features = ["rt", "time", "macros"] } | ||||
| tokio = { version = "1.45.1", features = ["rt", "time", "macros"] } | ||||
| futures = "0.3.31" | ||||
| ipnetwork = { version = "0.21.1", features = ["serde"] } | ||||
| num = "0.4.3" | ||||
|   | ||||
| @@ -182,6 +182,13 @@ impl Handler<DeleteDomainReq> for LibVirtActor { | ||||
|             false => sys::VIR_DOMAIN_UNDEFINE_NVRAM, | ||||
|         })?; | ||||
|  | ||||
|         // Delete associated cloud init disk | ||||
|         let cloud_init_disk = AppConfig::get().cloud_init_disk_path_for_vm(&domain_name); | ||||
|         if cloud_init_disk.exists() { | ||||
|             std::fs::remove_file(cloud_init_disk)?; | ||||
|         } | ||||
|  | ||||
|         // If requested, delete block storage associated with the VM | ||||
|         if !msg.keep_files { | ||||
|             log::info!("Delete storage associated with the domain"); | ||||
|             let path = AppConfig::get().vm_storage_path(msg.id); | ||||
|   | ||||
| @@ -250,6 +250,19 @@ impl AppConfig { | ||||
|         self.storage_path().join("iso") | ||||
|     } | ||||
|  | ||||
|     /// Get the path where generated cloud init disk image are stored | ||||
|     pub fn cloud_init_disk_storage_path(&self) -> PathBuf { | ||||
|         self.storage_path().join("cloud_init_disks") | ||||
|     } | ||||
|  | ||||
|     /// Get the path where the disk image of a VM is stored | ||||
|     pub fn cloud_init_disk_path_for_vm(&self, name: &str) -> PathBuf { | ||||
|         self.cloud_init_disk_storage_path().join(format!( | ||||
|             "{}-{name}.iso", | ||||
|             constants::CLOUD_INIT_IMAGE_PREFIX_NAME | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     /// Get disk images storage directory | ||||
|     pub fn disk_images_storage_path(&self) -> PathBuf { | ||||
|         self.storage_path().join("disk_images") | ||||
|   | ||||
| @@ -30,8 +30,9 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [ | ||||
| pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10); | ||||
|  | ||||
| /// Allowed uploaded disk images formats | ||||
| pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 3] = [ | ||||
| pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 4] = [ | ||||
|     "application/x-qemu-disk", | ||||
|     "application/x-raw-disk-image", | ||||
|     "application/gzip", | ||||
|     "application/octet-stream", | ||||
| ]; | ||||
| @@ -57,6 +58,9 @@ pub const DISK_SIZE_MIN: FileSize = FileSize::from_mb(50); | ||||
| /// Disk size max (B) | ||||
| pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000); | ||||
|  | ||||
| /// Cloud init generated disk image prefix | ||||
| pub const CLOUD_INIT_IMAGE_PREFIX_NAME: &str = "virtweb-cloudinit-autogen-image"; | ||||
|  | ||||
| /// Net nat entry comment max size | ||||
| pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250; | ||||
|  | ||||
|   | ||||
| @@ -55,7 +55,15 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>) | ||||
|     } | ||||
|  | ||||
|     // Copy the file to the destination | ||||
|     file.file.persist(dest_path)?; | ||||
|     file.file.persist(&dest_path)?; | ||||
|  | ||||
|     // Check if file information can be loaded | ||||
|     if let Err(e) = DiskFileInfo::load_file(&dest_path) { | ||||
|         log::error!("Failed to get information about uploaded disk file! {e}"); | ||||
|         std::fs::remove_file(&dest_path)?; | ||||
|         return Ok(HttpResponse::InternalServerError() | ||||
|             .json(format!("Unable to process uploaded file! {e}"))); | ||||
|     } | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json("Successfully uploaded disk image!")) | ||||
| } | ||||
|   | ||||
| @@ -109,6 +109,28 @@ pub async fn get_single_src_def(client: LibVirtReq, id: web::Path<SingleVMUUidRe | ||||
|         .body(info)) | ||||
| } | ||||
|  | ||||
| /// Get the generated cloud init configuration disk of a vm | ||||
| pub async fn get_cloud_init_disk(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> HttpResult { | ||||
|     let info = match client.get_single_domain(id.uid).await { | ||||
|         Ok(i) => i, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to get domain information! {e}"); | ||||
|             return Ok(HttpResponse::InternalServerError().json(e.to_string())); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let vm = VMInfo::from_domain(info)?; | ||||
|     let disk = vm.cloud_init.generate_nocloud_disk()?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok() | ||||
|         .content_type("application/x-iso9660-image") | ||||
|         .insert_header(( | ||||
|             "Content-Disposition", | ||||
|             format!("attachment; filename=\"cloud_init_{}.iso\"", vm.name), | ||||
|         )) | ||||
|         .body(disk)) | ||||
| } | ||||
|  | ||||
| /// Update a VM information | ||||
| pub async fn update( | ||||
|     client: LibVirtReq, | ||||
|   | ||||
| @@ -142,9 +142,22 @@ impl VMInfo { | ||||
|             return Err(StructureExtraction("Invalid number of vCPU specified!").into()); | ||||
|         } | ||||
|  | ||||
|         let mut disks = vec![]; | ||||
|         let mut iso_absolute_files = vec![]; | ||||
|  | ||||
|         // Add ISO files | ||||
|         // Process cloud init image | ||||
|         if self.cloud_init.attach_config { | ||||
|             let cloud_init_disk_path = AppConfig::get().cloud_init_disk_path_for_vm(&self.name); | ||||
|  | ||||
|             // Apply latest cloud init configuration | ||||
|             std::fs::write( | ||||
|                 &cloud_init_disk_path, | ||||
|                 self.cloud_init.generate_nocloud_disk()?, | ||||
|             )?; | ||||
|  | ||||
|             iso_absolute_files.push(cloud_init_disk_path); | ||||
|         } | ||||
|  | ||||
|         // Process uploaded ISO files | ||||
|         for iso_file in &self.iso_files { | ||||
|             if !files_utils::check_file_name(iso_file) { | ||||
|                 return Err(StructureExtraction("ISO filename is invalid!").into()); | ||||
| @@ -156,6 +169,13 @@ impl VMInfo { | ||||
|                 return Err(StructureExtraction("Specified ISO file does not exists!").into()); | ||||
|             } | ||||
|  | ||||
|             iso_absolute_files.push(path); | ||||
|         } | ||||
|  | ||||
|         let mut disks = vec![]; | ||||
|  | ||||
|         // Add ISO disk files | ||||
|         for iso_path in iso_absolute_files { | ||||
|             disks.push(DiskXML { | ||||
|                 r#type: "file".to_string(), | ||||
|                 device: "cdrom".to_string(), | ||||
| @@ -165,7 +185,7 @@ impl VMInfo { | ||||
|                     cache: "none".to_string(), | ||||
|                 }, | ||||
|                 source: DiskSourceXML { | ||||
|                     file: path.to_string_lossy().to_string(), | ||||
|                     file: iso_path.to_string_lossy().to_string(), | ||||
|                 }, | ||||
|                 target: DiskTargetXML { | ||||
|                     dev: format!( | ||||
| @@ -182,6 +202,7 @@ impl VMInfo { | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         // Configure VNC access, if requested | ||||
|         let (vnc_graphics, vnc_video) = match self.vnc_access { | ||||
|             true => ( | ||||
|                 Some(GraphicsXML { | ||||
| @@ -495,6 +516,7 @@ impl VMInfo { | ||||
|                 .iter() | ||||
|                 .filter(|d| d.device == "cdrom") | ||||
|                 .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string()) | ||||
|                 .filter(|d| !d.starts_with(constants::CLOUD_INIT_IMAGE_PREFIX_NAME)) | ||||
|                 .collect(), | ||||
|  | ||||
|             file_disks: domain | ||||
|   | ||||
| @@ -61,6 +61,8 @@ 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().cloud_init_disk_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(); | ||||
| @@ -202,6 +204,10 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/vm/{uid}/src", | ||||
|                 web::get().to(vm_controller::get_single_src_def), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/vm/{uid}/cloud_init_disk", | ||||
|                 web::get().to(vm_controller::get_cloud_init_disk), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/vm/{uid}/autostart", | ||||
|                 web::get().to(vm_controller::get_autostart), | ||||
|   | ||||
| @@ -1,19 +1,96 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use std::process::Command; | ||||
|  | ||||
| /// Cloud init DS Mode | ||||
| #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] | ||||
| pub enum CloudInitDSMode { | ||||
|     /// Networking is required | ||||
|     Net, | ||||
|     /// Does not require networking to be up before user-data actions are run | ||||
|     Local, | ||||
| } | ||||
|  | ||||
| /// VM Cloud Init configuration | ||||
| /// | ||||
| /// RedHat documentation: https://docs.redhat.com/fr/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_cloud-init_for_rhel_9/configuring-cloud-init_cloud-content | ||||
| /// cloud-localds source code: https://github.com/canonical/cloud-utils/blob/main/bin/cloud-localds | ||||
| #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)] | ||||
| pub struct CloudInitConfig { | ||||
|     attach_config: bool, | ||||
|     pub attach_config: bool, | ||||
|     /// Main user data | ||||
|     user_data: String, | ||||
|     pub user_data: String, | ||||
|     /// Instance ID, set in metadata file | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     instance_id: Option<String>, | ||||
|     pub instance_id: Option<String>, | ||||
|     /// Local hostname, set in metadata file | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     local_hostname: Option<String>, | ||||
|     pub local_hostname: Option<String>, | ||||
|     /// Data source mode | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub dsmode: Option<CloudInitDSMode>, | ||||
|     /// Network configuration | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     network_configuration: Option<String>, | ||||
|     pub network_configuration: Option<String>, | ||||
| } | ||||
|  | ||||
| impl CloudInitConfig { | ||||
|     /// Generate disk image for nocloud usage | ||||
|     pub fn generate_nocloud_disk(&self) -> anyhow::Result<Vec<u8>> { | ||||
|         let temp_path = tempfile::tempdir_in(&AppConfig::get().temp_dir)?; | ||||
|  | ||||
|         let mut cmd = Command::new(constants::PROGRAM_CLOUD_LOCALDS); | ||||
|  | ||||
|         // ISO destination path | ||||
|         let temp_iso = temp_path.path().join("disk.iso"); | ||||
|         cmd.arg(&temp_iso); | ||||
|  | ||||
|         // Process network configuration | ||||
|         if let Some(net_conf) = &self.network_configuration { | ||||
|             let net_conf_path = temp_path.path().join("network"); | ||||
|             std::fs::write(&net_conf_path, net_conf)?; | ||||
|             cmd.arg("--network-config").arg(&net_conf_path); | ||||
|         } | ||||
|  | ||||
|         // Process user data | ||||
|         let user_data_path = temp_path.path().join("user-data"); | ||||
|         std::fs::write(&user_data_path, &self.user_data)?; | ||||
|         cmd.arg(user_data_path); | ||||
|  | ||||
|         // Process metadata | ||||
|         let mut metadatas = vec![]; | ||||
|         if let Some(inst_id) = &self.instance_id { | ||||
|             metadatas.push(format!("instance-id: {}", inst_id)); | ||||
|         } | ||||
|         if let Some(local_hostname) = &self.local_hostname { | ||||
|             metadatas.push(format!("local-hostname: {}", local_hostname)); | ||||
|         } | ||||
|         if let Some(dsmode) = &self.dsmode { | ||||
|             metadatas.push(format!( | ||||
|                 "dsmode: {}", | ||||
|                 match dsmode { | ||||
|                     CloudInitDSMode::Net => "net", | ||||
|                     CloudInitDSMode::Local => "local", | ||||
|                 } | ||||
|             )); | ||||
|         } | ||||
|         let meta_data_path = temp_path.path().join("meta-data"); | ||||
|         std::fs::write(&meta_data_path, metadatas.join("\n"))?; | ||||
|         cmd.arg(meta_data_path); | ||||
|  | ||||
|         // Execute command | ||||
|         let output = cmd.output()?; | ||||
|         if !output.status.success() { | ||||
|             anyhow::bail!( | ||||
|                 "{} exited with status {}!\nStdout: {}\nStderr: {}", | ||||
|                 constants::PROGRAM_CLOUD_LOCALDS, | ||||
|                 output.status, | ||||
|                 String::from_utf8_lossy(&output.stdout), | ||||
|                 String::from_utf8_lossy(&output.stderr) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Read generated ISO file | ||||
|         Ok(std::fs::read(temp_iso)?) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -82,6 +82,15 @@ export interface VMNetBridge { | ||||
|   bridge: string; | ||||
| } | ||||
|  | ||||
| export interface VMCloudInit { | ||||
|   attach_config: boolean; | ||||
|   user_data: string; | ||||
|   instance_id?: string; | ||||
|   local_hostname?: string; | ||||
|   dsmode?: "Net" | "Local"; | ||||
|   network_configuration?: string; | ||||
| } | ||||
|  | ||||
| export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy"; | ||||
|  | ||||
| interface VMInfoInterface { | ||||
| @@ -101,6 +110,7 @@ interface VMInfoInterface { | ||||
|   networks: VMNetInterface[]; | ||||
|   tpm_module: boolean; | ||||
|   oem_strings: string[]; | ||||
|   cloud_init: VMCloudInit; | ||||
| } | ||||
|  | ||||
| export class VMInfo implements VMInfoInterface { | ||||
| @@ -120,6 +130,7 @@ export class VMInfo implements VMInfoInterface { | ||||
|   networks: VMNetInterface[]; | ||||
|   tpm_module: boolean; | ||||
|   oem_strings: string[]; | ||||
|   cloud_init: VMCloudInit; | ||||
|  | ||||
|   constructor(int: VMInfoInterface) { | ||||
|     this.name = int.name; | ||||
| @@ -138,6 +149,7 @@ export class VMInfo implements VMInfoInterface { | ||||
|     this.networks = int.networks; | ||||
|     this.tpm_module = int.tpm_module; | ||||
|     this.oem_strings = int.oem_strings; | ||||
|     this.cloud_init = int.cloud_init; | ||||
|   } | ||||
|  | ||||
|   static NewEmpty(): VMInfo { | ||||
| @@ -153,6 +165,7 @@ export class VMInfo implements VMInfoInterface { | ||||
|       networks: [], | ||||
|       tpm_module: true, | ||||
|       oem_strings: [], | ||||
|       cloud_init: { attach_config: false, user_data: "" }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -60,6 +60,7 @@ export function TokenRightsEditor(p: { | ||||
|               <TableCell align="center">Get XML definition</TableCell> | ||||
|               <TableCell align="center">Get autostart</TableCell> | ||||
|               <TableCell align="center">Set autostart</TableCell> | ||||
|               <TableCell align="center">Get CloudInit disk</TableCell> | ||||
|               <TableCell align="center">Backup disk</TableCell> | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
| @@ -84,6 +85,13 @@ export function TokenRightsEditor(p: { | ||||
|                 {...p} | ||||
|                 right={{ verb: "PUT", path: "/api/vm/*/autostart" }} | ||||
|               /> | ||||
|               <CellRight | ||||
|                 {...p} | ||||
|                 right={{ | ||||
|                   verb: "GET", | ||||
|                   path: "/api/vm/*/cloud_init_disk", | ||||
|                 }} | ||||
|               /> | ||||
|               <CellRight | ||||
|                 {...p} | ||||
|                 right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }} | ||||
| @@ -123,7 +131,15 @@ export function TokenRightsEditor(p: { | ||||
|                   {...p} | ||||
|                   right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }} | ||||
|                   parent={{ verb: "PUT", path: "/api/vm/*/autostart" }} | ||||
|                 />{" "} | ||||
|                 /> | ||||
|                 <CellRight | ||||
|                   {...p} | ||||
|                   right={{ | ||||
|                     verb: "GET", | ||||
|                     path: `/api/vm/${v.uuid}/cloud_init_disk`, | ||||
|                   }} | ||||
|                   parent={{ verb: "GET", path: "/api/vm/*/cloud_init_disk" }} | ||||
|                 /> | ||||
|                 <CellRight | ||||
|                   {...p} | ||||
|                   right={{ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user